diff --git a/internal/command/providers_lock.go b/internal/command/providers_lock.go index ba61146a4..a7243fab5 100644 --- a/internal/command/providers_lock.go +++ b/internal/command/providers_lock.go @@ -128,8 +128,8 @@ func (c *ProvidersLockCommand) Run(args []string) int { // current configuration. diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - "Invalid network mirror URL", - "The -net-mirror option requires a valid https: URL as the mirror base URL.", + "Invalid provider argument", + fmt.Sprintf("The provider %s is not required by the current configuration.", addr.String()), )) } } diff --git a/internal/command/providers_lock_test.go b/internal/command/providers_lock_test.go new file mode 100644 index 000000000..da6cca6ec --- /dev/null +++ b/internal/command/providers_lock_test.go @@ -0,0 +1,155 @@ +package command + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +func TestProvidersLock(t *testing.T) { + t.Run("noop", func(t *testing.T) { + // in the most basic case, running providers lock in a directory with no configuration at all should succeed. + // create an empty working directory + td := tempDir(t) + os.MkdirAll(td, 0755) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + c := &ProvidersLockCommand{ + Meta: Meta{ + Ui: ui, + }, + } + code := c.Run([]string{}) + if code != 0 { + t.Fatalf("wrong exit code; expected 0, got %d", code) + } + }) + + // This test depends on the -fs-mirror argument, so we always know what results to expect + t.Run("basic", func(t *testing.T) { + td := tempDir(t) + testCopyDir(t, testFixturePath("providers-lock/basic"), td) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Our fixture dir has a generic os_arch dir, which we need to customize + // to the actual OS/arch where this test is running in order to get the + // desired result. + fixtMachineDir := filepath.Join(td, "fs-mirror/registry.terraform.io/hashicorp/test/1.0.0/os_arch") + wantMachineDir := filepath.Join(td, "fs-mirror/registry.terraform.io/hashicorp/test/1.0.0/", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH)) + err := os.Rename(fixtMachineDir, wantMachineDir) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + p := testProvider() + ui := new(cli.MockUi) + c := &ProvidersLockCommand{ + Meta: Meta{ + Ui: ui, + testingOverrides: metaOverridesForProvider(p), + }, + } + + args := []string{"-fs-mirror=fs-mirror"} + code := c.Run(args) + if code != 0 { + t.Fatalf("wrong exit code; expected 0, got %d", code) + } + + lockfile, err := os.ReadFile(".terraform.lock.hcl") + if err != nil { + t.Fatal("error reading lockfile") + } + + expected := `# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/test" { + version = "1.0.0" + hashes = [ + "h1:7MjN4eFisdTv4tlhXH5hL4QQd39Jy4baPhFxwAd/EFE=", + ] +} +` + if string(lockfile) != expected { + t.Fatalf("wrong lockfile content") + } + }) +} + +func TestProvidersLock_args(t *testing.T) { + + t.Run("mirror collision", func(t *testing.T) { + ui := new(cli.MockUi) + c := &ProvidersLockCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + // only one of these arguments can be used at a time + args := []string{ + "-fs-mirror=/foo/", + "-net-mirror=www.foo.com", + } + code := c.Run(args) + + if code != 1 { + t.Fatalf("wrong exit code; expected 1, got %d", code) + } + output := ui.ErrorWriter.String() + if !strings.Contains(output, "The -fs-mirror and -net-mirror command line options are mutually-exclusive.") { + t.Fatalf("missing expected error message: %s", output) + } + }) + + t.Run("invalid platform", func(t *testing.T) { + ui := new(cli.MockUi) + c := &ProvidersLockCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + // not a valid platform + args := []string{"-platform=arbitrary_nonsense_that_isnt_valid"} + code := c.Run(args) + + if code != 1 { + t.Fatalf("wrong exit code; expected 1, got %d", code) + } + output := ui.ErrorWriter.String() + if !strings.Contains(output, "must be two words separated by an underscore.") { + t.Fatalf("missing expected error message: %s", output) + } + }) + + t.Run("invalid provider argument", func(t *testing.T) { + ui := new(cli.MockUi) + c := &ProvidersLockCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + // There is no configuration, so it's not valid to use any provider argument + args := []string{"hashicorp/random"} + code := c.Run(args) + + if code != 1 { + t.Fatalf("wrong exit code; expected 1, got %d", code) + } + output := ui.ErrorWriter.String() + if !strings.Contains(output, "The provider registry.terraform.io/hashicorp/random is not required by the\ncurrent configuration.") { + t.Fatalf("missing expected error message: %s", output) + } + }) +} diff --git a/internal/command/providers_mirror_test.go b/internal/command/providers_mirror_test.go new file mode 100644 index 000000000..073dd6f30 --- /dev/null +++ b/internal/command/providers_mirror_test.go @@ -0,0 +1,36 @@ +package command + +import ( + "strings" + "testing" + + "github.com/mitchellh/cli" +) + +// More thorough tests for providers mirror can be found in the e2etest +func TestProvidersMirror(t *testing.T) { + // noop example + t.Run("noop", func(t *testing.T) { + c := &ProvidersMirrorCommand{} + code := c.Run([]string{"."}) + if code != 0 { + t.Fatalf("wrong exit code. expected 0, got %d", code) + } + }) + + t.Run("missing arg error", func(t *testing.T) { + ui := new(cli.MockUi) + c := &ProvidersMirrorCommand{ + Meta: Meta{Ui: ui}, + } + code := c.Run([]string{}) + if code != 1 { + t.Fatalf("wrong exit code. expected 1, got %d", code) + } + + got := ui.ErrorWriter.String() + if !strings.Contains(got, "Error: No output directory specified") { + t.Fatalf("missing directory error from output, got:\n%s\n", got) + } + }) +} diff --git a/internal/command/testdata/providers-lock/basic/fs-mirror/registry.terraform.io/hashicorp/test/1.0.0/os_arch/terraform-provider-test b/internal/command/testdata/providers-lock/basic/fs-mirror/registry.terraform.io/hashicorp/test/1.0.0/os_arch/terraform-provider-test new file mode 100644 index 000000000..e69de29bb diff --git a/internal/command/testdata/providers-lock/basic/main.tf b/internal/command/testdata/providers-lock/basic/main.tf new file mode 100644 index 000000000..41b211f26 --- /dev/null +++ b/internal/command/testdata/providers-lock/basic/main.tf @@ -0,0 +1,7 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} \ No newline at end of file diff --git a/internal/getproviders/types.go b/internal/getproviders/types.go index a6498386b..28b1913d6 100644 --- a/internal/getproviders/types.go +++ b/internal/getproviders/types.go @@ -137,12 +137,12 @@ func (p Platform) LessThan(other Platform) bool { // ParsePlatform parses a string representation of a platform, like // "linux_amd64", or returns an error if the string is not valid. func ParsePlatform(str string) (Platform, error) { - underPos := strings.Index(str, "_") - if underPos < 1 || underPos >= len(str)-2 { + parts := strings.Split(str, "_") + if len(parts) != 2 { return Platform{}, fmt.Errorf("must be two words separated by an underscore") } - os, arch := str[:underPos], str[underPos+1:] + os, arch := parts[0], parts[1] if strings.ContainsAny(os, " \t\n\r") { return Platform{}, fmt.Errorf("OS portion must not contain whitespace") } diff --git a/internal/getproviders/types_test.go b/internal/getproviders/types_test.go index b12cc2155..0008793a1 100644 --- a/internal/getproviders/types_test.go +++ b/internal/getproviders/types_test.go @@ -94,3 +94,48 @@ func TestVersionConstraintsString(t *testing.T) { }) } } + +func TestParsePlatform(t *testing.T) { + tests := []struct { + Input string + Want Platform + Err bool + }{ + { + "", + Platform{}, + true, + }, + { + "too_many_notes", + Platform{}, + true, + }, + { + "extra _ whitespaces ", + Platform{}, + true, + }, + { + "arbitrary_os", + Platform{OS: "arbitrary", Arch: "os"}, + false, + }, + } + + for _, test := range tests { + got, err := ParsePlatform(test.Input) + if err != nil { + if test.Err == false { + t.Errorf("unexpected error: %s", err.Error()) + } + } else { + if test.Err { + t.Errorf("wrong result: expected error, got none") + } + } + if got != test.Want { + t.Errorf("wrong\n got: %q\nwant: %q", got, test.Want) + } + } +}