From d2e07f6f03c130fafdec5cf9b70a09d8c548d16d Mon Sep 17 00:00:00 2001 From: Nick Fagerlund Date: Thu, 30 Nov 2023 16:18:03 -0800 Subject: [PATCH] Add `TF_CLOUD_PLUGIN_DEV_OVERRIDE` to enable cloudplugin dev Currently, Terraform will only run properly signed versions of the experimental cloud plugin that were downloaded from a TFC instance that provides the appropriate service. That obstructs development on new cloud plugin features! Our internal teams will need a "dev override" capability, like what we offer provider authors. However, unlike with providers, we don't have to integrate this into a heterogeneous mix of sources for mirroring and caching a wide range of binaries. There's only one cloud plugin, HashiCorp controls it, and end users should never need to override the location of the binary for non-development reasons. Thus, we have the luxury of being quite a bit stupider in how we handle the override signal. Instead of adding it to the CLI config file schema, we'll just use a single environment variable whose value is the path to an alternate binary. Enter: `TF_CLOUD_PLUGIN_DEV_OVERRIDE`. --- internal/cloudplugin/binary.go | 27 ++++++++++++--- internal/cloudplugin/binary_test.go | 34 +++++++++++++++---- internal/cloudplugin/testdata/cloudplugin-dev | 1 + internal/command/cloud.go | 19 +++++++++-- 4 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 internal/cloudplugin/testdata/cloudplugin-dev diff --git a/internal/cloudplugin/binary.go b/internal/cloudplugin/binary.go index 7818d91b4b..0d576a5143 100644 --- a/internal/cloudplugin/binary.go +++ b/internal/cloudplugin/binary.go @@ -27,6 +27,7 @@ type BinaryManager struct { signingKey string binaryName string cloudPluginDataDir string + overridePath string host svchost.Hostname client *CloudPluginClient goos string @@ -37,9 +38,10 @@ type BinaryManager struct { // Binary is a struct containing the path to an authenticated binary corresponding to // a backend service. type Binary struct { - Path string - ProductVersion string - ResolvedFromCache bool + Path string + ProductVersion string + ResolvedFromCache bool + ResolvedFromDevOverride bool } const ( @@ -50,7 +52,7 @@ const ( // BinaryManager initializes a new BinaryManager to broker data between the // specified directory location containing cloudplugin package data and a // Terraform Cloud backend URL. -func NewBinaryManager(ctx context.Context, cloudPluginDataDir string, serviceURL *url.URL, goos, arch string) (*BinaryManager, error) { +func NewBinaryManager(ctx context.Context, cloudPluginDataDir, overridePath string, serviceURL *url.URL, goos, arch string) (*BinaryManager, error) { client, err := NewCloudPluginClient(ctx, serviceURL) if err != nil { return nil, fmt.Errorf("could not initialize cloudplugin version manager: %w", err) @@ -58,6 +60,7 @@ func NewBinaryManager(ctx context.Context, cloudPluginDataDir string, serviceURL return &BinaryManager{ cloudPluginDataDir: cloudPluginDataDir, + overridePath: overridePath, host: svchost.Hostname(serviceURL.Host), client: client, binaryName: "terraform-cloudplugin", @@ -90,6 +93,22 @@ func (v BinaryManager) cachedVersion(version string) *string { // Resolve fetches, authenticates, and caches a plugin binary matching the specifications // and returns its location and version. func (v BinaryManager) Resolve() (*Binary, error) { + if v.overridePath != "" { + log.Printf("[TRACE] Using dev override for cloudplugin binary") + return v.resolveDev() + } + return v.resolveRelease() +} + +func (v BinaryManager) resolveDev() (*Binary, error) { + return &Binary{ + Path: v.overridePath, + ProductVersion: "dev", + ResolvedFromDevOverride: true, + }, nil +} + +func (v BinaryManager) resolveRelease() (*Binary, error) { manifest, err := v.latestManifest(v.ctx) if err != nil { return nil, fmt.Errorf("could not resolve cloudplugin version for host %q: %w", v.host.ForDisplay(), err) diff --git a/internal/cloudplugin/binary_test.go b/internal/cloudplugin/binary_test.go index 40140015b5..f14df7b8b9 100644 --- a/internal/cloudplugin/binary_test.go +++ b/internal/cloudplugin/binary_test.go @@ -11,7 +11,7 @@ import ( "testing" ) -func assertResolvedBinary(t *testing.T, binary *Binary, assertCached bool) { +func assertResolvedBinary(t *testing.T, binary *Binary, assertCached, assertOverridden bool) { t.Helper() if binary == nil { @@ -22,6 +22,10 @@ func assertResolvedBinary(t *testing.T, binary *Binary, assertCached bool) { t.Errorf("expected ResolvedFromCache to be %v, got %v", assertCached, binary.ResolvedFromCache) } + if binary.ResolvedFromDevOverride != assertOverridden { + t.Errorf("expected ResolvedFromDevOverride to be %v, got %v", assertOverridden, binary.ResolvedFromDevOverride) + } + info, err := os.Stat(binary.Path) if err != nil { t.Fatalf("expected no error when getting binary location, got %q", err) @@ -31,8 +35,15 @@ func assertResolvedBinary(t *testing.T, binary *Binary, assertCached bool) { t.Fatalf("expected non-zero file at %q", binary.Path) } - if binary.ProductVersion != "0.1.0" { // from sample manifest - t.Errorf("expected product binary %q, got %q", "0.1.0", binary.ProductVersion) + var expectedVersion string + if assertOverridden { + expectedVersion = "dev" + } else { + expectedVersion = "0.1.0" + } + + if binary.ProductVersion != expectedVersion { // from sample manifest + t.Errorf("expected product binary %q, got %q", expectedVersion, binary.ProductVersion) } } @@ -52,7 +63,7 @@ func TestBinaryManager_Resolve(t *testing.T) { serviceURL := serverURL.JoinPath("/api/cloudplugin/v1") tempDir := t.TempDir() - manager, err := NewBinaryManager(context.Background(), tempDir, serviceURL, "darwin", "amd64") + manager, err := NewBinaryManager(context.Background(), tempDir, "", serviceURL, "darwin", "amd64") if err != nil { t.Fatalf("expected no err, got: %s", err) } @@ -64,7 +75,7 @@ func TestBinaryManager_Resolve(t *testing.T) { t.Fatalf("expected no err, got %s", err) } - assertResolvedBinary(t, binary, false) + assertResolvedBinary(t, binary, false, false) // Resolving a second time should return a cached binary binary, err = manager.Resolve() @@ -72,7 +83,7 @@ func TestBinaryManager_Resolve(t *testing.T) { t.Fatalf("expected no err, got %s", err) } - assertResolvedBinary(t, binary, true) + assertResolvedBinary(t, binary, true, false) // Change the local binary data err = os.WriteFile(filepath.Join(filepath.Dir(binary.Path), ".version"), []byte("0.0.9"), 0644) @@ -85,5 +96,14 @@ func TestBinaryManager_Resolve(t *testing.T) { t.Fatalf("expected no err, got %s", err) } - assertResolvedBinary(t, binary, false) + assertResolvedBinary(t, binary, false, false) + + // Set a dev override + manager.overridePath = "testdata/cloudplugin-dev" + binary, err = manager.Resolve() + if err != nil { + t.Fatalf("expected no err, got %s", err) + } + + assertResolvedBinary(t, binary, false, true) } diff --git a/internal/cloudplugin/testdata/cloudplugin-dev b/internal/cloudplugin/testdata/cloudplugin-dev new file mode 100644 index 0000000000..ce12f244b3 --- /dev/null +++ b/internal/cloudplugin/testdata/cloudplugin-dev @@ -0,0 +1 @@ +i have deleted the toucan diff --git a/internal/command/cloud.go b/internal/command/cloud.go index b6eb10ea56..10fa96e57e 100644 --- a/internal/command/cloud.go +++ b/internal/command/cloud.go @@ -63,8 +63,10 @@ func (c *CloudCommand) realRun(args []string, stdout, stderr io.Writer) int { args = c.Meta.process(args) diags := c.initPlugin() + if diags.HasWarnings() || diags.HasErrors() { + c.View.Diagnostics(diags) + } if diags.HasErrors() { - c.Ui.Warn(diags.ErrWithWarnings().Error()) return ExitPluginError } @@ -191,7 +193,9 @@ func (c *CloudCommand) initPlugin() tfdiags.Diagnostics { return diags.Append(tfdiags.Sourceless(tfdiags.Error, errorSummary, err.Error())) } - bm, err := cloudplugin.NewBinaryManager(ctx, packagesPath, serviceURL, runtime.GOOS, runtime.GOARCH) + overridePath := os.Getenv("TF_CLOUD_PLUGIN_DEV_OVERRIDE") + + bm, err := cloudplugin.NewBinaryManager(ctx, packagesPath, overridePath, serviceURL, runtime.GOOS, runtime.GOARCH) if err != nil { return diags.Append(tfdiags.Sourceless(tfdiags.Error, errorSummary, err.Error())) } @@ -205,9 +209,18 @@ func (c *CloudCommand) initPlugin() tfdiags.Diagnostics { if version.ResolvedFromCache { cacheTraceMsg = " (resolved from cache)" } + if version.ResolvedFromDevOverride { + cacheTraceMsg = " (resolved from dev override)" + detailMsg := fmt.Sprintf("Instead of using the current released version, Terraform is loading the cloud plugin from the following location:\n\n - %s\n\nOverriding the cloud plugin location can cause unexpected behavior, and is only intended for use when developing new versions of the plugin.", version.Path) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Cloud plugin development overrides are in effect", + detailMsg, + )) + } log.Printf("[TRACE] plugin %q binary located at %q%s", version.ProductVersion, version.Path, cacheTraceMsg) c.pluginBinary = version.Path - return nil + return diags } func (c *CloudCommand) initPackagesCache() (string, error) {