diff --git a/USAGE.md b/USAGE.md index c0c5530a..a41583c2 100644 --- a/USAGE.md +++ b/USAGE.md @@ -10,6 +10,10 @@ You may want to override `$ENVOY_VERSIONS_URL` to supply custom builds or otherwise control the source of Envoy binaries. When overriding, validate your JSON first: https://archive.tetratelabs.io/release-versions-schema.json +Advanced: +`FUNC_E_PLATFORM` overrides the host OS and architecture of Envoy binaries. +This value must be constant within a `$FUNC_E_HOME`. + # Commands | Name | Usage | @@ -26,3 +30,4 @@ your JSON first: https://archive.tetratelabs.io/release-versions-schema.json | ---- | ----- | ------- | | FUNC_E_HOME | func-e home directory (location of installed versions and run archives) | ${HOME}/.func-e | | ENVOY_VERSIONS_URL | URL of Envoy versions JSON | https://archive.tetratelabs.io/envoy/envoy-versions.json | +| FUNC_E_PLATFORM | the host OS and architecture of Envoy binaries. Ex. darwin/arm64 | $GOOS/$GOARCH | diff --git a/internal/cmd/app.go b/internal/cmd/app.go index 3361f212..eef390a2 100644 --- a/internal/cmd/app.go +++ b/internal/cmd/app.go @@ -29,7 +29,7 @@ import ( // NewApp create a new root command. The globals.GlobalOpts parameter allows tests to scope overrides, which avoids // having to define a flag for everything needed in tests. func NewApp(o *globals.GlobalOpts) *cli.App { - var envoyVersionsURL, homeDir string + var envoyVersionsURL, homeDir, platform string lastKnownEnvoy := getLastKnownEnvoy(o) app := cli.NewApp() @@ -47,7 +47,11 @@ func NewApp(o *globals.GlobalOpts) *cli.App { You may want to override ` + "`$ENVOY_VERSIONS_URL`" + ` to supply custom builds or otherwise control the source of Envoy binaries. When overriding, validate - your JSON first: https://archive.tetratelabs.io/release-versions-schema.json` + your JSON first: https://archive.tetratelabs.io/release-versions-schema.json + + Advanced: + ` + "`FUNC_E_PLATFORM`" + ` overrides the host OS and architecture of Envoy binaries. + This value must be constant within a ` + "`$FUNC_E_HOME`" + `.` app.Version = string(o.Version) app.Flags = []cli.Flag{ &cli.StringFlag{ @@ -63,8 +67,17 @@ func NewApp(o *globals.GlobalOpts) *cli.App { DefaultText: globals.DefaultEnvoyVersionsURL, Destination: &envoyVersionsURL, EnvVars: []string{"ENVOY_VERSIONS_URL"}, - }} + }, + &cli.StringFlag{ + Name: "platform", + Usage: "the host OS and architecture of Envoy binaries. Ex. darwin/arm64", + DefaultText: "$GOOS/$GOARCH", + Destination: &platform, + EnvVars: []string{"FUNC_E_PLATFORM"}, + }, + } app.Before = func(c *cli.Context) error { + setPlatform(o, platform) if err := setHomeDir(o, homeDir); err != nil { return err } @@ -103,6 +116,17 @@ var helpCommand = &cli.Command{ }, } +func setPlatform(o *globals.GlobalOpts, platform string) { + if o.Platform != "" { // overridden for tests + return + } + if platform != "" { // set by user + o.Platform = version.Platform(platform) + } else { + o.Platform = globals.DefaultPlatform + } +} + func setEnvoyVersionsURL(o *globals.GlobalOpts, versionsURL string) error { if o.EnvoyVersionsURL != "" { // overridden for tests return nil diff --git a/internal/cmd/app_test.go b/internal/cmd/app_test.go index 5ed596e1..d1176ee4 100644 --- a/internal/cmd/app_test.go +++ b/internal/cmd/app_test.go @@ -114,6 +114,49 @@ func TestHomeDir(t *testing.T) { } } +func TestPlatformArg(t *testing.T) { + type testCase struct { + name string + args []string + // setup returns a tear-down function + setup func() func() + expected version.Platform + } + + tests := []testCase{ + { + name: "FUNC_E_PLATFORM env", + args: []string{"func-e"}, + setup: func() func() { + return morerequire.RequireSetenv(t, "FUNC_E_PLATFORM", "linux/amd64") + }, + expected: version.Platform("linux/amd64"), + }, + { + name: "--platform flag", + args: []string{"func-e", "--platform", "darwin/amd64"}, + expected: version.Platform("darwin/amd64"), + }, + } + + for _, tc := range tests { + tc := tc // pin! see https://github.com/kyoh86/scopelint for why + + t.Run(tc.name, func(t *testing.T) { + if tc.setup != nil { + tearDown := tc.setup() + defer tearDown() + } + + o := &globals.GlobalOpts{} + err := runTestCommand(t, o, tc.args) + require.NoError(t, err) + require.Equal(t, tc.expected, o.Platform) + }) + } + +} + func TestEnvoyVersionsURL(t *testing.T) { type testCase struct { name string diff --git a/internal/cmd/run.go b/internal/cmd/run.go index a696e9bb..2692c717 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -71,7 +71,7 @@ state. On exit, these archive into ` + "`$FUNC_E_HOME/runs/$epochtime.tar.gz`", return nil }, Action: func(c *cli.Context) error { - if err := initializeRunOpts(c.Context, o, globals.CurrentPlatform, envoyVersion); err != nil { + if err := initializeRunOpts(c.Context, o, envoyVersion); err != nil { return err } r := envoy.NewRuntime(&o.RunOpts) @@ -111,10 +111,10 @@ state. On exit, these archive into ` + "`$FUNC_E_HOME/runs/$epochtime.tar.gz`", // initializeRunOpts allows us to default values when not overridden for tests. // The version parameter correlates with the globals.GlobalOpts EnvoyPath which is installed if needed. // Notably, this creates and sets a globals.GlobalOpts WorkingDirectory for Envoy, and any files that precede it. -func initializeRunOpts(ctx context.Context, o *globals.GlobalOpts, p version.Platform, v version.Version) error { +func initializeRunOpts(ctx context.Context, o *globals.GlobalOpts, v version.Version) error { runOpts := &o.RunOpts if o.EnvoyPath == "" { // not overridden for tests - envoyPath, err := envoy.InstallIfNeeded(ctx, o, p, v) + envoyPath, err := envoy.InstallIfNeeded(ctx, o, v) if err != nil { return err } @@ -144,7 +144,7 @@ func setHomeEnvoyVersion(ctx context.Context, o *globals.GlobalOpts) error { // First time install: look up the latest version, which may be newer than version.LastKnownEnvoy! fmt.Fprintln(o.Out, "looking up latest version") //nolint - m, err := envoy.FuncEVersions(ctx, o.EnvoyVersionsURL, globals.CurrentPlatform, o.Version) + m, err := envoy.FuncEVersions(ctx, o.EnvoyVersionsURL, o.Platform, o.Version) if err != nil { return NewValidationError(`couldn't read latest version from %s: %s`, o.EnvoyVersionsURL, err) } diff --git a/internal/cmd/testdata/func-e_help.txt b/internal/cmd/testdata/func-e_help.txt index 5407ce0e..99d2cae9 100644 --- a/internal/cmd/testdata/func-e_help.txt +++ b/internal/cmd/testdata/func-e_help.txt @@ -13,6 +13,10 @@ USAGE: otherwise control the source of Envoy binaries. When overriding, validate your JSON first: https://archive.tetratelabs.io/release-versions-schema.json + Advanced: + `FUNC_E_PLATFORM` overrides the host OS and architecture of Envoy binaries. + This value must be constant within a `$FUNC_E_HOME`. + VERSION: 1.0 @@ -25,4 +29,5 @@ COMMANDS: GLOBAL OPTIONS: --home-dir value func-e home directory (location of installed versions and run archives) (default: ${HOME}/.func-e) [$FUNC_E_HOME] --envoy-versions-url value URL of Envoy versions JSON (default: https://archive.tetratelabs.io/envoy/envoy-versions.json) [$ENVOY_VERSIONS_URL] + --platform value the host OS and architecture of Envoy binaries. Ex. darwin/arm64 (default: $GOOS/$GOARCH) [$FUNC_E_PLATFORM] --version, -v print the version (default: false) diff --git a/internal/cmd/use.go b/internal/cmd/use.go index 707569a8..3134d4c0 100644 --- a/internal/cmd/use.go +++ b/internal/cmd/use.go @@ -44,7 +44,7 @@ $ func-e use %s`, envoy.CurrentVersionWorkingDirFile, envoy.CurrentVersionHomeDi Before: validateVersionArg, Action: func(c *cli.Context) error { v := version.Version(c.Args().First()) - if _, err := envoy.InstallIfNeeded(c.Context, o, globals.CurrentPlatform, v); err != nil { + if _, err := envoy.InstallIfNeeded(c.Context, o, v); err != nil { return err } return envoy.WriteCurrentVersion(v, o.HomeDir) diff --git a/internal/cmd/versions.go b/internal/cmd/versions.go index ab06e603..d767f0ae 100644 --- a/internal/cmd/versions.go +++ b/internal/cmd/versions.go @@ -50,9 +50,9 @@ func NewVersionsCmd(o *globals.GlobalOpts) *cli.Command { currentVersion, currentVersionSource, _ := envoy.CurrentVersion(o.HomeDir) if c.Bool("all") { - if ev, err := envoy.FuncEVersions(c.Context, o.EnvoyVersionsURL, globals.CurrentPlatform, o.Version); err != nil { + if ev, err := envoy.FuncEVersions(c.Context, o.EnvoyVersionsURL, o.Platform, o.Version); err != nil { return err - } else if err := addAvailableVersions(&rows, ev.Versions, globals.CurrentPlatform); err != nil { + } else if err := addAvailableVersions(&rows, ev.Versions, o.Platform); err != nil { return err } } diff --git a/internal/envoy/http_test.go b/internal/envoy/http_test.go index da51ee25..4924fb7e 100644 --- a/internal/envoy/http_test.go +++ b/internal/envoy/http_test.go @@ -33,7 +33,7 @@ func TestHttpGet_AddsDefaultHeaders(t *testing.T) { })) defer ts.Close() - res, err := httpGet(context.Background(), ts.URL, globals.CurrentPlatform, "dev") + res, err := httpGet(context.Background(), ts.URL, globals.DefaultPlatform, "dev") require.NoError(t, err) defer res.Body.Close() diff --git a/internal/envoy/install.go b/internal/envoy/install.go index f1852678..d8d964c2 100644 --- a/internal/envoy/install.go +++ b/internal/envoy/install.go @@ -35,39 +35,39 @@ import ( var binEnvoy = filepath.Join("bin", "envoy"+moreos.Exe) // InstallIfNeeded downloads an Envoy binary corresponding to the given version and returns a path to it or an error. -func InstallIfNeeded(ctx context.Context, o *globals.GlobalOpts, p version.Platform, v version.Version) (string, error) { +func InstallIfNeeded(ctx context.Context, o *globals.GlobalOpts, v version.Version) (string, error) { installPath := filepath.Join(o.HomeDir, "versions", string(v)) envoyPath := filepath.Join(installPath, binEnvoy) _, err := os.Stat(envoyPath) switch { case os.IsNotExist(err): var ev version.ReleaseVersions // Get version metadata for what we will install - ev, err = FuncEVersions(ctx, o.EnvoyVersionsURL, p, v) + ev, err = FuncEVersions(ctx, o.EnvoyVersionsURL, o.Platform, v) if err != nil { return "", err } - tarballURL := ev.Versions[v].Tarballs[p] // Ensure there is a version for this platform + tarballURL := ev.Versions[v].Tarballs[o.Platform] // Ensure there is a version for this platform if tarballURL == "" { - return "", fmt.Errorf("couldn't find version %q for platform %q", v, p) + return "", fmt.Errorf("couldn't find version %q for platform %q", v, o.Platform) } tarball := version.Tarball(path.Base(string(tarballURL))) sha256Sum := ev.SHA256Sums[tarball] if len(sha256Sum) != 64 { - return "", fmt.Errorf("couldn't find sha256Sum of version %q for platform %q: %w", v, p, err) + return "", fmt.Errorf("couldn't find sha256Sum of version %q for platform %q: %w", v, o.Platform, err) } var mtime time.Time // Create a directory for the version, preserving the release date as its mtime if mtime, err = time.Parse("2006-01-02", string(ev.Versions[v].ReleaseDate)); err != nil { - return "", fmt.Errorf("couldn't find releaseDate of version %q for platform %q: %w", v, p, err) + return "", fmt.Errorf("couldn't find releaseDate of version %q for platform %q: %w", v, o.Platform, err) } if err = os.MkdirAll(installPath, 0750); err != nil { return "", fmt.Errorf("unable to create directory %q: %w", installPath, err) } - fmt.Fprintln(o.Out, "downloading", tarballURL) //nolint - if err = untarEnvoy(ctx, installPath, tarballURL, sha256Sum, p, v); err != nil { //nolint + fmt.Fprintln(o.Out, "downloading", tarballURL) //nolint + if err = untarEnvoy(ctx, installPath, tarballURL, sha256Sum, o.Platform, v); err != nil { //nolint return "", err } if err = os.Chtimes(installPath, mtime, mtime); err != nil { // overwrite the mtime to preserve it in the list diff --git a/internal/envoy/install_test.go b/internal/envoy/install_test.go index bdf5cee4..63424997 100644 --- a/internal/envoy/install_test.go +++ b/internal/envoy/install_test.go @@ -54,7 +54,7 @@ func TestUntarEnvoyError(t *testing.T) { url := version.TarballURL(server.URL + "/file.tar.gz") t.Run("error on incorrect URL", func(t *testing.T) { - err := untarEnvoy(ctx, dst, url, tarballSHA256sum, globals.CurrentPlatform, "dev") + err := untarEnvoy(ctx, dst, url, tarballSHA256sum, globals.DefaultPlatform, "dev") require.EqualError(t, err, fmt.Sprintf(`received 404 status code from %s`, url)) }) @@ -62,7 +62,7 @@ func TestUntarEnvoyError(t *testing.T) { w.WriteHeader(200) } t.Run("error on empty", func(t *testing.T) { - err := untarEnvoy(ctx, dst, url, tarballSHA256sum, globals.CurrentPlatform, "dev") + err := untarEnvoy(ctx, dst, url, tarballSHA256sum, globals.DefaultPlatform, "dev") require.EqualError(t, err, fmt.Sprintf(`error untarring %s: EOF`, url)) }) @@ -71,7 +71,7 @@ func TestUntarEnvoyError(t *testing.T) { w.Write([]byte("mary had a little lamb")) //nolint } t.Run("error on not a tar", func(t *testing.T) { - err := untarEnvoy(ctx, dst, url, tarballSHA256sum, globals.CurrentPlatform, "dev") + err := untarEnvoy(ctx, dst, url, tarballSHA256sum, globals.DefaultPlatform, "dev") require.EqualError(t, err, fmt.Sprintf(`error untarring %s: gzip: invalid header`, url)) }) @@ -80,7 +80,7 @@ func TestUntarEnvoyError(t *testing.T) { w.Write(tarball) //nolint } t.Run("error on wrong sha256sum a tar", func(t *testing.T) { - err := untarEnvoy(ctx, dst, url, "cafebabe", globals.CurrentPlatform, "dev") + err := untarEnvoy(ctx, dst, url, "cafebabe", globals.DefaultPlatform, "dev") require.EqualError(t, err, fmt.Sprintf(`expected SHA-256 sum "cafebabe", but have "%s" from %s`, tarballSHA256sum, url)) }) } @@ -99,7 +99,7 @@ func TestUntarEnvoy(t *testing.T) { })) defer server.Close() - err := untarEnvoy(context.Background(), tempDir, version.TarballURL(server.URL), tarballSHA256sum, globals.CurrentPlatform, "dev") + err := untarEnvoy(context.Background(), tempDir, version.TarballURL(server.URL), tarballSHA256sum, globals.DefaultPlatform, "dev") require.NoError(t, err) require.FileExists(t, filepath.Join(tempDir, binEnvoy)) } @@ -110,7 +110,7 @@ func TestInstallIfNeeded_ErrorOnIncorrectURL(t *testing.T) { o.EnvoyVersionsURL += "/varsionz.json" - _, err := InstallIfNeeded(o.ctx, &o.GlobalOpts, globals.CurrentPlatform, version.LastKnownEnvoy) + _, err := InstallIfNeeded(o.ctx, &o.GlobalOpts, version.LastKnownEnvoy) require.EqualError(t, err, "received 404 status code from "+o.EnvoyVersionsURL) require.Empty(t, o.Out.(*bytes.Buffer)) } @@ -141,9 +141,10 @@ func TestInstallIfNeeded_Validates(t *testing.T) { for _, tt := range tests { tc := tt + o.Platform = tt.p t.Run(tc.name, func(t *testing.T) { o.Out = new(bytes.Buffer) - _, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, tc.p, tc.v) + _, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, tc.v) require.EqualError(t, e, tc.expectedErr) require.Empty(t, o.Out.(*bytes.Buffer)) }) @@ -155,7 +156,7 @@ func TestInstallIfNeeded(t *testing.T) { defer cleanup() out := o.Out.(*bytes.Buffer) - envoyPath, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, globals.CurrentPlatform, version.LastKnownEnvoy) + envoyPath, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, version.LastKnownEnvoy) require.NoError(t, e) require.Equal(t, o.EnvoyPath, envoyPath) require.FileExists(t, envoyPath) @@ -164,7 +165,7 @@ func TestInstallIfNeeded(t *testing.T) { versionDir := strings.Replace(envoyPath, binEnvoy, "", 1) f, err := os.Stat(versionDir) require.NoError(t, err) - require.Equal(t, f.ModTime().Format("2006-01-02"), string(test.FakeReleaseDate)) + require.Equal(t, f.ModTime().UTC().Format("2006-01-02"), string(test.FakeReleaseDate)) require.Equal(t, fmt.Sprintln("downloading", o.tarballURL), out.String()) } @@ -174,11 +175,13 @@ func TestInstallIfNeeded_NotFound(t *testing.T) { defer cleanup() t.Run("unknown version", func(t *testing.T) { - _, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, "darwin/amd64", "1.1.1") + o.Platform = "darwin/amd64" + _, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, "1.1.1") require.EqualError(t, e, `couldn't find version "1.1.1" for platform "darwin/amd64"`) }) t.Run("unknown platform", func(t *testing.T) { - _, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, "solaris/amd64", version.LastKnownEnvoy) + o.Platform = "solaris/amd64" + _, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, version.LastKnownEnvoy) require.EqualError(t, e, fmt.Sprintf(`couldn't find version "%s" for platform "solaris/amd64"`, version.LastKnownEnvoy)) }) } @@ -194,7 +197,7 @@ func TestInstallIfNeeded_AlreadyExists(t *testing.T) { envoyStat, err := os.Stat(o.EnvoyPath) require.NoError(t, err) - envoyPath, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, globals.CurrentPlatform, version.LastKnownEnvoy) + envoyPath, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, version.LastKnownEnvoy) require.NoError(t, e) require.Equal(t, fmt.Sprintln(version.LastKnownEnvoy, "is already downloaded"), out.String()) @@ -257,6 +260,7 @@ func setupInstallTest(t *testing.T) (*installTest, func()) { HomeDir: tempDir, EnvoyVersionsURL: versionsServer.URL + "/envoy-versions.json", Out: new(bytes.Buffer), + Platform: globals.DefaultPlatform, RunOpts: globals.RunOpts{ EnvoyPath: filepath.Join(tempDir, "versions", string(version.LastKnownEnvoy), binEnvoy), }, diff --git a/internal/globals/globals.go b/internal/globals/globals.go index 03718ddb..b96baf93 100644 --- a/internal/globals/globals.go +++ b/internal/globals/globals.go @@ -58,6 +58,8 @@ type GlobalOpts struct { HomeDir string // Out is where status messages are written. Defaults to os.Stdout Out io.Writer + // The platform to target for the Envoy install. + Platform version.Platform } const ( @@ -65,11 +67,11 @@ const ( DefaultHomeDir = "${HOME}/.func-e" // DefaultEnvoyVersionsURL is the default value for GlobalOpts.EnvoyVersionsURL DefaultEnvoyVersionsURL = "https://archive.tetratelabs.io/envoy/envoy-versions.json" + // DefaultPlatform is the current platform of the host machine + DefaultPlatform = version.Platform(runtime.GOOS + "/" + runtime.GOARCH) ) var ( // EnvoyVersionPattern is used to validate versions and is the same pattern as release-versions-schema.json. EnvoyVersionPattern = regexp.MustCompile(`^[1-9][0-9]*\.[0-9]+\.[0-9]+(_debug)?$`) - // CurrentPlatform is the platform of the current process. This is used as a key in EnvoyVersion.Tarballs. - CurrentPlatform = version.Platform(runtime.GOOS + "/" + runtime.GOARCH) )