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`.
pull/34353/head
Nick Fagerlund 2 years ago
parent 9a7fae5ff1
commit d2e07f6f03

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

@ -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)
}

@ -0,0 +1 @@
i have deleted the toucan

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

Loading…
Cancel
Save