From 00667800495bcc898f5bbd4c5240909199e3cf62 Mon Sep 17 00:00:00 2001 From: Daniel Schmidt Date: Fri, 13 Feb 2026 14:40:44 +0100 Subject: [PATCH] refactor providers mirror command argument parsing --- .../command/arguments/providers_mirror.go | 48 ++++++++ .../arguments/providers_mirror_test.go | 114 ++++++++++++++++++ internal/command/providers_mirror.go | 43 ++----- 3 files changed, 172 insertions(+), 33 deletions(-) create mode 100644 internal/command/arguments/providers_mirror.go create mode 100644 internal/command/arguments/providers_mirror_test.go diff --git a/internal/command/arguments/providers_mirror.go b/internal/command/arguments/providers_mirror.go new file mode 100644 index 0000000000..a51ad15254 --- /dev/null +++ b/internal/command/arguments/providers_mirror.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import "github.com/hashicorp/terraform/internal/tfdiags" + +// ProvidersMirror represents the command-line arguments for the providers +// mirror command. +type ProvidersMirror struct { + Platforms FlagStringSlice + LockFile bool + OutputDir string +} + +// ParseProvidersMirror processes CLI arguments, returning a ProvidersMirror +// value and errors. If errors are encountered, a ProvidersMirror value is +// still returned representing the best effort interpretation of the arguments. +func ParseProvidersMirror(args []string) (*ProvidersMirror, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + providersMirror := &ProvidersMirror{} + + cmdFlags := defaultFlagSet("providers mirror") + cmdFlags.Var(&providersMirror.Platforms, "platform", "target platform") + cmdFlags.BoolVar(&providersMirror.LockFile, "lock-file", true, "use lock file") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + args = cmdFlags.Args() + + if len(args) != 1 { + return providersMirror, diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No output directory specified", + "The providers mirror command requires an output directory as a command-line argument.", + )) + } + + providersMirror.OutputDir = args[0] + + return providersMirror, diags +} diff --git a/internal/command/arguments/providers_mirror_test.go b/internal/command/arguments/providers_mirror_test.go new file mode 100644 index 0000000000..b776abd85b --- /dev/null +++ b/internal/command/arguments/providers_mirror_test.go @@ -0,0 +1,114 @@ +// 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 TestParseProvidersMirror_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *ProvidersMirror + }{ + "defaults": { + []string{"./mirror"}, + &ProvidersMirror{ + LockFile: true, + OutputDir: "./mirror", + }, + }, + "all options": { + []string{ + "-platform=linux_amd64", + "-platform=darwin_arm64", + "-lock-file=false", + "./mirror", + }, + &ProvidersMirror{ + Platforms: FlagStringSlice{"linux_amd64", "darwin_arm64"}, + LockFile: false, + OutputDir: "./mirror", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseProvidersMirror(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 TestParseProvidersMirror_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *ProvidersMirror + wantDiags tfdiags.Diagnostics + }{ + "missing output directory": { + nil, + &ProvidersMirror{ + LockFile: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "No output directory specified", + "The providers mirror command requires an output directory as a command-line argument.", + ), + }, + }, + "too many arguments": { + []string{"./mirror", "./extra"}, + &ProvidersMirror{ + LockFile: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "No output directory specified", + "The providers mirror command requires an output directory as a command-line argument.", + ), + }, + }, + "unknown flag and missing output directory": { + []string{"-wat"}, + &ProvidersMirror{ + LockFile: true, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -wat", + ), + tfdiags.Sourceless( + tfdiags.Error, + "No output directory specified", + "The providers mirror command requires an output directory as a command-line argument.", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseProvidersMirror(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_mirror.go b/internal/command/providers_mirror.go index 80162c59f9..cfff1b2e2c 100644 --- a/internal/command/providers_mirror.go +++ b/internal/command/providers_mirror.go @@ -32,41 +32,18 @@ func (c *ProvidersMirrorCommand) Synopsis() string { } func (c *ProvidersMirrorCommand) Run(args []string) int { - args = c.Meta.process(args) - cmdFlags := c.Meta.defaultFlagSet("providers mirror") - - var optPlatforms arguments.FlagStringSlice - cmdFlags.Var(&optPlatforms, "platform", "target platform") - - var optLockFile bool - cmdFlags.BoolVar(&optLockFile, "lock-file", true, "use lock file") - - 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 - - args = cmdFlags.Args() - if len(args) != 1 { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "No output directory specified", - "The providers mirror command requires an output directory as a command-line argument.", - )) + parsedArgs, diags := arguments.ParseProvidersMirror(c.Meta.process(args)) + if diags.HasErrors() { c.showDiagnostics(diags) return 1 } - outputDir := args[0] 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( @@ -94,7 +71,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { diags = diags.Append(lockedDepsDiags) // If lock file is present, validate it against configuration - if !lockedDeps.Empty() && optLockFile { + if !lockedDeps.Empty() && parsedArgs.LockFile { if errs := config.VerifyDependencySelections(lockedDeps); len(errs) > 0 { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -166,7 +143,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { continue } selected := candidates.Newest() - if !lockedDeps.Empty() && optLockFile { + if !lockedDeps.Empty() && parsedArgs.LockFile { selected = lockedDeps.Provider(provider).Version() c.Ui.Output(fmt.Sprintf(" - Selected v%s to match dependency lock file", selected.String())) } else if len(constraintsStr) > 0 { @@ -214,7 +191,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { // so we can verify its checksums and signatures before making // it discoverable to mirror clients. (stagingPath intentionally // does not follow the filesystem mirror file naming convention.) - targetPath := meta.PackedFilePath(outputDir) + targetPath := meta.PackedFilePath(parsedArgs.OutputDir) stagingPath := filepath.Join(filepath.Dir(targetPath), "."+filepath.Base(targetPath)) err = httpGetter.GetFile(stagingPath, urlObj) if err != nil { @@ -255,7 +232,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { // by relying on the selections we made above, because we want to still // include in the indices any packages that were already present and // not affected by the changes we just made. - available, err := getproviders.SearchLocalDirectory(outputDir) + available, err := getproviders.SearchLocalDirectory(parsedArgs.OutputDir) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -273,7 +250,7 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { // we'll ask the getproviders package to build an archive filename // for a fictitious package and then use the directory portion of it. indexDir := filepath.Dir(getproviders.PackedFilePathForPackage( - outputDir, provider, versions.Unspecified, getproviders.CurrentPlatform, + parsedArgs.OutputDir, provider, versions.Unspecified, getproviders.CurrentPlatform, )) indexVersions := map[string]interface{}{} indexArchives := map[getproviders.Version]map[string]interface{}{}