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) {