diff --git a/flag.go b/flag.go index a4b4544..4114e1b 100644 --- a/flag.go +++ b/flag.go @@ -35,7 +35,7 @@ type FlagSet struct { flagProviderName string flagBackendType string flagBackendConfig cli.StringSlice - flagFullConfig bool + flagConfigMode string flagMaskSensitive bool flagParallelism int flagContinue bool @@ -144,8 +144,8 @@ func (flag FlagSet) DescribeCLI(mode Mode) string { if flag.flagBackendType != "" { args = append(args, "--backend-type="+flag.flagBackendType) } - if flag.flagFullConfig { - args = append(args, "--full-properties=true") + if flag.flagConfigMode != "" && flag.flagConfigMode != string(config.ConfigModeMinimal) { + args = append(args, "--config-mode="+flag.flagConfigMode) } if flag.flagMaskSensitive { args = append(args, "--mask-sensitive=true") @@ -458,7 +458,7 @@ func (f FlagSet) BuildCommonConfig() (config.CommonConfig, error) { ContinueOnError: f.flagContinue, BackendType: f.flagBackendType, BackendConfig: f.flagBackendConfig.Value(), - FullConfig: f.flagFullConfig, + ConfigMode: config.ConfigMode(f.flagConfigMode), MaskSensitive: f.flagMaskSensitive, Parallelism: f.flagParallelism, HCLOnly: f.flagHCLOnly, diff --git a/go.mod b/go.mod index c6f1393..0e2a17e 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/magodo/spinner v0.0.0-20240524082745-3a2305db1bdc github.com/magodo/terraform-client-go v0.0.0-20240804032252-6d93a97fabb2 github.com/magodo/textinput v0.0.0-20210913072708-7d24f2b4b0c0 - github.com/magodo/tfadd v0.10.1-0.20260402010906-8f7ab2866ec6 + github.com/magodo/tfadd v0.10.1-0.20260625074521-bfdd14b3d25b github.com/magodo/tfmerge v0.0.0-20221214062955-f52e46d03402 github.com/magodo/tfstate v0.0.0-20241016043929-2c95177bf0e6 github.com/magodo/workerpool v0.0.0-20240524082508-11838001bc35 diff --git a/go.sum b/go.sum index 427394c..854ba42 100644 --- a/go.sum +++ b/go.sum @@ -272,8 +272,8 @@ github.com/magodo/terraform-client-go v0.0.0-20240804032252-6d93a97fabb2 h1:X2ph github.com/magodo/terraform-client-go v0.0.0-20240804032252-6d93a97fabb2/go.mod h1:dzTs6Qwy8/VUWYMQ+Vo9gMXe5uOVbfCw/1ovE3g5q1E= github.com/magodo/textinput v0.0.0-20210913072708-7d24f2b4b0c0 h1:aNtr4iNv/tex2t8W1u3scAoNHEnFlTKhNNHOpYStqbs= github.com/magodo/textinput v0.0.0-20210913072708-7d24f2b4b0c0/go.mod h1:MqYhNP+PC386Bjsx5piZe7T4vDm5QIPv8b1RU0prVnU= -github.com/magodo/tfadd v0.10.1-0.20260402010906-8f7ab2866ec6 h1:aD83e/Lol5MVEE3PosjacwADZjc1kBDD4YPzUut2UxE= -github.com/magodo/tfadd v0.10.1-0.20260402010906-8f7ab2866ec6/go.mod h1:Ul9tfUbNokUeWnT/xzwvqxxma7mJRMG5pUjz2H/cY1w= +github.com/magodo/tfadd v0.10.1-0.20260625074521-bfdd14b3d25b h1:xcv6g2IXUVURA5T3wgJTPPJgn3aOShnn1+AsXMCuWVw= +github.com/magodo/tfadd v0.10.1-0.20260625074521-bfdd14b3d25b/go.mod h1:Ul9tfUbNokUeWnT/xzwvqxxma7mJRMG5pUjz2H/cY1w= github.com/magodo/tfmerge v0.0.0-20221214062955-f52e46d03402 h1:RyaR4VE7hoR9AyoVH414cpM8V63H4rLe2aZyKdoDV1w= github.com/magodo/tfmerge v0.0.0-20221214062955-f52e46d03402/go.mod h1:ssV++b4DH33rsD592bvpS4Peng3ZfdGNZbFgCDkCfj8= github.com/magodo/tfpluginschema v0.0.0-20240902090353-0525d7d8c1c2 h1:Unxx8WLxzSxINnq7hItp4cXD7drihgfPltTd91efoBo= diff --git a/internal/meta/base_meta.go b/internal/meta/base_meta.go index ef16071..64c701c 100644 --- a/internal/meta/base_meta.go +++ b/internal/meta/base_meta.go @@ -105,7 +105,7 @@ type baseMeta struct { providerConfig map[string]cty.Value // tfadd options - fullConfig bool + configMode config.ConfigMode maskSensitive bool parallelism int @@ -287,6 +287,22 @@ func NewBaseMeta(cfg config.CommonConfig) (*baseMeta, error) { excludeAzureResources = append(excludeAzureResources, *re) } + // Resolve ConfigMode. + configMode := cfg.ConfigMode + switch configMode { + case "": + configMode = config.ConfigModeMinimal + case config.ConfigModeMinimal, config.ConfigModeLossless, config.ConfigModeFull: + // ok + default: + return nil, fmt.Errorf("invalid ConfigMode %q: must be one of %q, %q, %q", + cfg.ConfigMode, + config.ConfigModeMinimal, + config.ConfigModeLossless, + config.ConfigModeFull, + ) + } + meta := &baseMeta{ logger: cfg.Logger, subscriptionId: cfg.SubscriptionId, @@ -301,7 +317,7 @@ func NewBaseMeta(cfg config.CommonConfig) (*baseMeta, error) { backendConfig: cfg.BackendConfig, providerConfig: providerConfig, providerName: cfg.ProviderName, - fullConfig: cfg.FullConfig, + configMode: configMode, maskSensitive: cfg.MaskSensitive, parallelism: cfg.Parallelism, preImportHook: cfg.PreImportHook, @@ -1029,6 +1045,37 @@ func (meta *baseMeta) importItem_notf(ctx context.Context, item *ImportItem, imp return } +// tfaddOptions translates the configured ConfigMode (plus the provider in use) +// into the set of tfadd options that drive trimming behaviour. +func (meta baseMeta) tfaddOptions() []tfadd.OptionSetter { + opts := []tfadd.OptionSetter{ + tfadd.MaskSenstitive(meta.maskSensitive), + } + + switch meta.configMode { + case config.ConfigModeFull: + opts = append(opts, tfadd.Full(true)) + case config.ConfigModeLossless: + opts = append(opts, + tfadd.KeepOC(true), + // The azurerm provider currently uses Terraform Plugin SDKv2, which tolerates + // the mismatch between null and zero values, so zero values can be safely + // trimmed. + // The azapi provider uses Plugin Framework, which does NOT tolerate + // that mismatch, so zero values must be kept to produce a semantically + // equivalent config. + tfadd.KeepZero(meta.useAzAPI()), + ) + case config.ConfigModeMinimal: + opts = append(opts, + // Same here. + tfadd.KeepZero(meta.useAzAPI()), + ) + } + + return opts +} + func (meta baseMeta) stateToConfig(ctx context.Context, list ImportList) (ConfigInfos, error) { var out []ConfigInfo var bs [][]byte @@ -1039,6 +1086,7 @@ func (meta baseMeta) stateToConfig(ctx context.Context, list ImportList) (Config if meta.useAzAPI() { providerName = "registry.terraform.io/azure/azapi" } + tfaddOpts := meta.tfaddOptions() if meta.tfclient != nil { for _, item := range importedList { @@ -1059,8 +1107,7 @@ func (meta baseMeta) stateToConfig(ctx context.Context, list ImportList) (Config ProviderName: providerName, Value: item.State, }, - tfadd.Full(meta.fullConfig), - tfadd.MaskSenstitive(meta.maskSensitive), + tfaddOpts..., ) if err != nil { return nil, fmt.Errorf("generating state for resource %s: %v", item.TFAddr, err) @@ -1078,7 +1125,7 @@ func (meta baseMeta) stateToConfig(ctx context.Context, list ImportList) (Config } var err error - bs, err = tfadd.StateForTargets(ctx, meta.tf, addrs, tfadd.Full(meta.fullConfig), tfadd.MaskSenstitive(meta.maskSensitive)) + bs, err = tfadd.StateForTargets(ctx, meta.tf, addrs, tfaddOpts...) if err != nil { return nil, fmt.Errorf("converting terraform state to config: %w", err) } @@ -1222,4 +1269,3 @@ func appendToFile(path, content string) error { _, err = f.WriteString(content) return err } - diff --git a/main.go b/main.go index 515e9df..89fe25f 100644 --- a/main.go +++ b/main.go @@ -155,12 +155,12 @@ func main() { Usage: "The Terraform backend config", Destination: &flagset.flagBackendConfig, }, - &cli.BoolFlag{ - Name: "full-properties", - EnvVars: []string{"AZTFEXPORT_FULL_PROPERTIES"}, - Usage: "Includes all non-computed properties in the Terraform configuration. This may require manual modifications to produce a valid config", - Value: false, - Destination: &flagset.flagFullConfig, + &cli.StringFlag{ + Name: "config-mode", + EnvVars: []string{"AZTFEXPORT_CONFIG_MODE"}, + Usage: `The trimming mode for the generated Terraform config. Can be one of "minimal" (most aggressive; trims zero values, schema defaults and Optional+Computed attributes), "lossless" (keeps Optional+Computed attributes so the config matches the live state) and "full" (keeps every property; may require manual edits to be valid)`, + Value: string(config.ConfigModeMinimal), + Destination: &flagset.flagConfigMode, }, &cli.BoolFlag{ Name: "mask-sensitive", diff --git a/pkg/config/config.go b/pkg/config/config.go index 274e7b8..c99f8af 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -40,6 +40,27 @@ type OutputFileNames struct { ImportBlockFileName string } +// ConfigMode controls how aggressively the generated Terraform configuration +// trims properties read back from the resource state. +type ConfigMode string + +const ( + // ConfigModeMinimal generates the smallest possible config by removing + // zero values, schema defaults, and Optional+Computed + // attributes/blocks. + ConfigModeMinimal ConfigMode = "minimal" + + // ConfigModeLossless keeps Optional+Computed attributes/blocks (so the + // generated config matches the live state), while still removing schema + // defaults and (for SDKv2 providers) zero values that can be safely + // dropped without changing semantics. + ConfigModeLossless ConfigMode = "lossless" + + // ConfigModeFull keeps every property, including zero values, schema + // defaults, and Optional+Computed attributes/blocks. + ConfigModeFull ConfigMode = "full" +) + type CommonConfig struct { Logger *slog.Logger // AuthConfig specifies the authentication config for provider @@ -73,8 +94,8 @@ type CommonConfig struct { // This is not used directly by aztfexport as the provider configs can be set by environment variable already. // While it is useful for module users that want support multi-users scenarios in one process (in which case changing env vars affect the whole process). ProviderConfig map[string]cty.Value - // FullConfig specifies whether to export all (non computed-only) Terarform properties when generating TF configs. - FullConfig bool + // ConfigMode controls how aggressively the generated TF config is trimmed. Defaults to ConfigModeMinimal. + ConfigMode ConfigMode // MaskSensitive specifies whether to mask sensitive attributes when generating TF configs. MaskSensitive bool // Parallelism specifies the parallelism for the process