From 9a74064d2bc4bd5ff01b6969263fe536e25d007b Mon Sep 17 00:00:00 2001 From: Paul Stack Date: Thu, 10 Sep 2020 19:25:47 +0100 Subject: [PATCH] Allow passing a non-default secrets provider to AutomationAPI (#5320) --- CHANGELOG.md | 3 ++ sdk/go/x/auto/example_test.go | 22 ++++++++ sdk/go/x/auto/local_workspace.go | 32 +++++++++--- sdk/go/x/auto/local_workspace_test.go | 73 ++++++++++++++++++++++++++- 4 files changed, 123 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2029ff12..eace990e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ CHANGELOG - Python SDK: Add support for `Sequence[T]` for array types [#5282](https://github.com/pulumi/pulumi/pull/5282) + +- feat(autoapi): Add support for non default secret providers in local workspaces + [#5320](https://github.com/pulumi/pulumi/pull/5320) ## 2.9.2 (2020-08-31) diff --git a/sdk/go/x/auto/example_test.go b/sdk/go/x/auto/example_test.go index ba3a54f47..8a7381efe 100644 --- a/sdk/go/x/auto/example_test.go +++ b/sdk/go/x/auto/example_test.go @@ -312,6 +312,28 @@ func ExampleNewLocalWorkspace() { _, _ = NewLocalWorkspace(ctx, wd, ph, proj) } +func ExampleLocalWorkspace_secretsProvider() { + ctx := context.Background() + // WorkDir sets the working directory for the LocalWorkspace. The workspace will look for a default + // project settings file (Pulumi.yaml) in this location for information about the Pulumi program. + wd := WorkDir(filepath.Join("..", "path", "to", "pulumi", "project")) + // PulumiHome customizes the location of $PULUMI_HOME where metadata is stored and plugins are installed. + ph := PulumiHome(filepath.Join("~", ".pulumi")) + // Project provides ProjectSettings to set once the workspace is created. + proj := Project(workspace.Project{ + Name: tokens.PackageName("myproject"), + Runtime: workspace.NewProjectRuntimeInfo("go", nil), + Backend: &workspace.ProjectBackend{ + URL: "https://url.to.custom.saas.backend.com", + }, + }) + // Secrets provider provides a way of passing a non-default secrets provider to the + // workspace and the stacks created from it. Supported secrets providers are: + // `awskms`, `azurekeyvault`, `gcpkms`, `hashivault` and `passphrase` + secretsProvider := SecretsProvider("awskms://alias/mysecretkeyalias") + _, _ = NewLocalWorkspace(ctx, wd, ph, proj, secretsProvider) +} + func ExampleLocalWorkspace_ListPlugins() { ctx := context.Background() // create a workspace from a local project diff --git a/sdk/go/x/auto/local_workspace.go b/sdk/go/x/auto/local_workspace.go index c947f5ad5..ee4eb8b45 100644 --- a/sdk/go/x/auto/local_workspace.go +++ b/sdk/go/x/auto/local_workspace.go @@ -39,10 +39,11 @@ import ( // alter the Workspace Pulumi.yaml file, and setting config on a Stack will modify the Pulumi..yaml file. // This is identical to the behavior of Pulumi CLI driven workspaces. type LocalWorkspace struct { - workDir string - pulumiHome string - program pulumi.RunFunc - envvars map[string]string + workDir string + pulumiHome string + program pulumi.RunFunc + envvars map[string]string + secretsProvider string } var settingsExtensions = []string{".yaml", ".yml", ".json"} @@ -136,7 +137,7 @@ func (l *LocalWorkspace) GetConfig(ctx context.Context, fqsn string, key string) if err != nil { return val, errors.Wrapf(err, "could not get config, unable to select stack %s", fqsn) } - stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "config", "get", key, "--show-secrets", "--json") + stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "config", "get", key, "--json") if err != nil { return val, newAutoError(errors.Wrap(err, "unable to read config"), stdout, stderr, errCode) } @@ -330,7 +331,11 @@ func (l *LocalWorkspace) CreateStack(ctx context.Context, fqsn string) error { return errors.Wrap(err, "failed to create stack") } - stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "stack", "init", fqsn) + args := []string{"stack", "init", fqsn} + if l.secretsProvider != "" { + args = append(args, fmt.Sprintf("--secrets-provider=%s", l.secretsProvider)) + } + stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, args...) if err != nil { return newAutoError(errors.Wrap(err, "failed to create stack"), stdout, stderr, errCode) } @@ -526,6 +531,11 @@ func NewLocalWorkspace(ctx context.Context, opts ...LocalWorkspaceOption) (Works } } + // Secrets providers + if lwOpts.SecretsProvider != "" { + l.secretsProvider = lwOpts.SecretsProvider + } + return l, nil } @@ -545,6 +555,8 @@ type localWorkspaceOptions struct { Stacks map[string]workspace.ProjectStack // Repo is a git repo with a Pulumi Project to clone into the WorkDir. Repo *GitRepo + // Secrets Provider to use with the current Stack + SecretsProvider string } // LocalWorkspaceOption is used to customize and configure a LocalWorkspace at initialization time. @@ -621,6 +633,14 @@ func Repo(gitRepo GitRepo) LocalWorkspaceOption { }) } +// SecretsProvider is the secrets provider to use with the current +// workspace when interacting with a stack +func SecretsProvider(secretsProvider string) LocalWorkspaceOption { + return localWorkspaceOption(func(lo *localWorkspaceOptions) { + lo.SecretsProvider = secretsProvider + }) +} + // ValidateFullyQualifiedStackName validates that the fqsn is in the form "org/project/name". func ValidateFullyQualifiedStackName(fqsn string) error { parts := strings.Split(fqsn, "/") diff --git a/sdk/go/x/auto/local_workspace_test.go b/sdk/go/x/auto/local_workspace_test.go index e73e91539..12fba00f2 100644 --- a/sdk/go/x/auto/local_workspace_test.go +++ b/sdk/go/x/auto/local_workspace_test.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "math/rand" + "os" "os/exec" "path/filepath" "testing" @@ -30,9 +31,79 @@ import ( "github.com/stretchr/testify/assert" ) -const pulumiOrg = "pulumi" +const pulumiOrg = "moolumi" const pName = "testproj" +func TestWorkspaceSecretsProvider(t *testing.T) { + ctx := context.Background() + sName := fmt.Sprintf("int_test%d", rangeIn(10000000, 99999999)) + fqsn := FullyQualifiedStackName(pulumiOrg, pName, sName) + + // We can't use Workspace EnvVars as the Workspace uses the secrets provider to + // create the Stack + err := os.Setenv("PULUMI_CONFIG_PASSPHRASE", "password") + assert.Nil(t, err, "failed to set EnvVar.") + + // initialize + s, err := NewStackInlineSource(ctx, fqsn, func(ctx *pulumi.Context) error { + c := config.New(ctx, "") + ctx.Export("exp_static", pulumi.String("foo")) + ctx.Export("exp_cfg", pulumi.String(c.Get("bar"))) + ctx.Export("exp_secret", c.GetSecret("buzz")) + return nil + }, SecretsProvider("passphrase")) + if err != nil { + t.Errorf("failed to initialize stack, err: %v", err) + t.FailNow() + } + + defer func() { + err := os.Unsetenv("PULUMI_CONFIG_PASSPHRASE") + assert.Nil(t, err, "failed to unset EnvVar.") + + // -- pulumi stack rm -- + err = s.Workspace().RemoveStack(ctx, s.Name()) + assert.Nil(t, err, "failed to remove stack. Resources have leaked.") + }() + + passwordVal := "Password1234!" + err = s.SetConfig(ctx, "MySecretDatabasePassword", ConfigValue{Value: passwordVal, Secret: true}) + if err != nil { + t.Errorf("setConfig failed, err: %v", err) + t.FailNow() + } + + // -- pulumi up -- + res, err := s.Up(ctx) + if err != nil { + t.Errorf("up failed, err: %v", err) + t.FailNow() + } + + assert.Equal(t, "update", res.Summary.Kind) + assert.Equal(t, "succeeded", res.Summary.Result) + + // -- get config -- + conf, err := s.GetConfig(ctx, "MySecretDatabasePassword") + if err != nil { + t.Errorf("GetConfig failed, err: %v", err) + t.FailNow() + } + assert.Equal(t, passwordVal, conf.Value) + assert.Equal(t, true, conf.Secret) + + // -- pulumi destroy -- + + dRes, err := s.Destroy(ctx) + if err != nil { + t.Errorf("destroy failed, err: %v", err) + t.FailNow() + } + + assert.Equal(t, "destroy", dRes.Summary.Kind) + assert.Equal(t, "succeeded", dRes.Summary.Result) +} + func TestNewStackLocalSource(t *testing.T) { ctx := context.Background() sName := fmt.Sprintf("int_test%d", rangeIn(10000000, 99999999))