From af7783eb624fe79cf74a44e8d1194a49458d85f0 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Fri, 13 Feb 2026 14:40:44 +0100 Subject: [PATCH] refactor providers lock command argument parsing --- internal/command/arguments/providers_lock.go | 52 ++++++++ .../command/arguments/providers_lock_test.go | 124 ++++++++++++++++++ internal/command/providers_lock.go | 52 ++------ 3 files changed, 189 insertions(+), 39 deletions(-) create mode 100644 internal/command/arguments/providers_lock.go create mode 100644 internal/command/arguments/providers_lock_test.go diff --git a/internal/command/arguments/providers_lock.go b/internal/command/arguments/providers_lock.go new file mode 100644 index 0000000000..6ba2610ef2 --- /dev/null +++ b/internal/command/arguments/providers_lock.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import "github.com/hashicorp/terraform/internal/tfdiags" + +// ProvidersLock represents the command-line arguments for the providers lock +// command. +type ProvidersLock struct { + Platforms FlagStringSlice + FSMirrorDir string + NetMirrorURL string + TestsDirectory string + EnablePluginCache bool + Providers []string +} + +// ParseProvidersLock processes CLI arguments, returning a ProvidersLock value +// and errors. If errors are encountered, a ProvidersLock value is still +// returned representing the best effort interpretation of the arguments. +func ParseProvidersLock(args []string) (*ProvidersLock, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + providersLock := &ProvidersLock{} + + cmdFlags := defaultFlagSet("providers lock") + cmdFlags.Var(&providersLock.Platforms, "platform", "target platform") + cmdFlags.StringVar(&providersLock.FSMirrorDir, "fs-mirror", "", "filesystem mirror directory") + cmdFlags.StringVar(&providersLock.NetMirrorURL, "net-mirror", "", "network mirror base URL") + cmdFlags.StringVar(&providersLock.TestsDirectory, "test-directory", "tests", "test-directory") + cmdFlags.BoolVar(&providersLock.EnablePluginCache, "enable-plugin-cache", false, "") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + if providersLock.FSMirrorDir != "" && providersLock.NetMirrorURL != "" { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid installation method options", + "The -fs-mirror and -net-mirror command line options are mutually-exclusive.", + )) + } + + providersLock.Providers = cmdFlags.Args() + + return providersLock, diags +} diff --git a/internal/command/arguments/providers_lock_test.go b/internal/command/arguments/providers_lock_test.go new file mode 100644 index 0000000000..7dbf13ea16 --- /dev/null +++ b/internal/command/arguments/providers_lock_test.go @@ -0,0 +1,124 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseProvidersLock_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *ProvidersLock + }{ + "defaults": { + nil, + &ProvidersLock{ + TestsDirectory: "tests", + }, + }, + "all options": { + []string{ + "-platform=linux_amd64", + "-platform=darwin_arm64", + "-fs-mirror=mirror", + "-test-directory=integration-tests", + "-enable-plugin-cache", + "hashicorp/test", + }, + &ProvidersLock{ + Platforms: FlagStringSlice{"linux_amd64", "darwin_arm64"}, + FSMirrorDir: "mirror", + TestsDirectory: "integration-tests", + EnablePluginCache: true, + Providers: []string{"hashicorp/test"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseProvidersLock(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("unexpected result\n%s", diff) + } + }) + } +} + +func TestParseProvidersLock_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *ProvidersLock + wantDiags tfdiags.Diagnostics + }{ + "mirror collision": { + []string{ + "-fs-mirror=foo", + "-net-mirror=https://example.com", + }, + &ProvidersLock{ + FSMirrorDir: "foo", + NetMirrorURL: "https://example.com", + TestsDirectory: "tests", + Providers: []string{}, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Invalid installation method options", + "The -fs-mirror and -net-mirror command line options are mutually-exclusive.", + ), + }, + }, + "unknown flag": { + []string{"-wat"}, + &ProvidersLock{ + TestsDirectory: "tests", + Providers: []string{}, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -wat", + ), + }, + }, + "unknown flag and mirror collision": { + []string{ + "-wat", + "-fs-mirror=foo", + "-net-mirror=https://example.com", + }, + &ProvidersLock{ + TestsDirectory: "tests", + Providers: []string{"-fs-mirror=foo", "-net-mirror=https://example.com"}, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -wat", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseProvidersLock(tc.args) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatalf("unexpected result\n%s", diff) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/providers_lock.go b/internal/command/providers_lock.go index 1a1d66897c..a13a1bc86e 100644 --- a/internal/command/providers_lock.go +++ b/internal/command/providers_lock.go @@ -39,44 +39,18 @@ func (c *ProvidersLockCommand) Synopsis() string { } func (c *ProvidersLockCommand) Run(args []string) int { - args = c.Meta.process(args) - cmdFlags := c.Meta.defaultFlagSet("providers lock") - var optPlatforms arguments.FlagStringSlice - var fsMirrorDir string - var netMirrorURL string - var testDirectory string - - cmdFlags.Var(&optPlatforms, "platform", "target platform") - cmdFlags.StringVar(&fsMirrorDir, "fs-mirror", "", "filesystem mirror directory") - cmdFlags.StringVar(&netMirrorURL, "net-mirror", "", "network mirror base URL") - cmdFlags.StringVar(&testDirectory, "test-directory", "tests", "test-directory") - pluginCache := cmdFlags.Bool("enable-plugin-cache", false, "") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) - return 1 - } - - var diags tfdiags.Diagnostics - - if fsMirrorDir != "" && netMirrorURL != "" { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid installation method options", - "The -fs-mirror and -net-mirror command line options are mutually-exclusive.", - )) + parsedArgs, diags := arguments.ParseProvidersLock(c.Meta.process(args)) + if diags.HasErrors() { c.showDiagnostics(diags) return 1 } - providerStrs := cmdFlags.Args() - var platforms []getproviders.Platform - if len(optPlatforms) == 0 { + if len(parsedArgs.Platforms) == 0 { platforms = []getproviders.Platform{getproviders.CurrentPlatform} } else { - platforms = make([]getproviders.Platform, 0, len(optPlatforms)) - for _, platformStr := range optPlatforms { + platforms = make([]getproviders.Platform, 0, len(parsedArgs.Platforms)) + for _, platformStr := range parsedArgs.Platforms { platform, err := getproviders.ParsePlatform(platformStr) if err != nil { diags = diags.Append(tfdiags.Sourceless( @@ -104,10 +78,10 @@ func (c *ProvidersLockCommand) Run(args []string) int { // against the upstream checksums. var source getproviders.Source switch { - case fsMirrorDir != "": - source = getproviders.NewFilesystemMirrorSource(fsMirrorDir) - case netMirrorURL != "": - u, err := url.Parse(netMirrorURL) + case parsedArgs.FSMirrorDir != "": + source = getproviders.NewFilesystemMirrorSource(parsedArgs.FSMirrorDir) + case parsedArgs.NetMirrorURL != "": + u, err := url.Parse(parsedArgs.NetMirrorURL) if err != nil || u.Scheme != "https" { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -125,7 +99,7 @@ func (c *ProvidersLockCommand) Run(args []string) int { source = getproviders.NewRegistrySource(c.Services) } - config, confDiags := c.loadConfigWithTests(".", testDirectory) + config, confDiags := c.loadConfigWithTests(".", parsedArgs.TestsDirectory) diags = diags.Append(confDiags) reqs, hclDiags := config.ProviderRequirements() diags = diags.Append(hclDiags) @@ -134,9 +108,9 @@ func (c *ProvidersLockCommand) Run(args []string) int { // we'll modify "reqs" to only include those. Modifying this is okay // because config.ProviderRequirements generates a fresh map result // for each call. - if len(providerStrs) != 0 { + if len(parsedArgs.Providers) != 0 { providers := map[addrs.Provider]struct{}{} - for _, raw := range providerStrs { + for _, raw := range parsedArgs.Providers { addr, moreDiags := addrs.ParseProviderSourceString(raw) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { @@ -253,7 +227,7 @@ func (c *ProvidersLockCommand) Run(args []string) int { // Use global plugin cache for extra speed if present and flag is set globalCacheDir := c.providerGlobalCacheDir() - if *pluginCache && globalCacheDir != nil { + if parsedArgs.EnablePluginCache && globalCacheDir != nil { installer.SetGlobalCacheDir(globalCacheDir.WithPlatform(platform)) installer.SetGlobalCacheDirMayBreakDependencyLockFile(c.PluginCacheMayBreakDependencyLockFile) }