diff --git a/docs/content/credentials.md b/docs/content/credentials.md index c47c245ee..1273775eb 100644 --- a/docs/content/credentials.md +++ b/docs/content/credentials.md @@ -44,6 +44,37 @@ where the values can be found. If you are creating credential sets manually, you can use the [Credential Set Schema] to validate that you have created it properly. +### Remembering Credentials +Porter remembers the last set of credentials used with an installation, and reuses them when the bundle is executed again. + +For example, if Carolyn installs a bundle using her credentials, Porter remembers that Carolyn's credentials are associated with the resulting installation. +Now when Carolyn upgrades the bundle, if credentials are not specified, Porter will reuse the original credentials that the bundle was installed with. +Later Yingrong upgrades the bundle, specifying the shared team credentials in the upgrade command, and now those credentials are associated with the installation instead of Carolyn's personal credentials. + +```console +$ porter install tutorial -c carolyn-creds -r ghcr.io/getporter/examples/credentials-tutorial:v0.2.0 +# bundle is installed with Carolyn's credentials + +$ porter show tutorial +Name: tutorial +Namespace: quickstart +Created: 3 minutes ago +Modified: 7 seconds ago + +Bundle: + Repository: ghcr.io/getporter/examples/credentials-tutorial + Version: 0.2.0 + +Credential Sets: + - carolyn-creds + +$ porter upgrade --version 0.3.0 +# Carolyn's credentials are used again, since none were specified + +$ porter upgrade -c blue-team-creds --version 0.3.1 +# Upgrade is run again but this time with the shared blue team credentials +``` + [Credential Set Schema]: /src/pkg/schema/credential-set.schema.json ## Runtime diff --git a/pkg/porter/helpers.go b/pkg/porter/helpers.go index ec1a9af19..61c8a499c 100644 --- a/pkg/porter/helpers.go +++ b/pkg/porter/helpers.go @@ -134,7 +134,12 @@ func (p *TestPorter) SetupIntegrationTest() context.Context { err = encoding.UnmarshalYaml(ciCredsB, &testCreds) require.NoError(t, err, "could not unmarshal test credentials %s", ciCredsPath) err = p.Credentials.UpsertCredentialSet(context.Background(), testCreds) - require.NoError(t, err, "could not save test credentials") + require.NoError(t, err, "could not save test credentials (ci)") + + // Make a copy of the creds with a different name so that we can test out switching to different credential sets + testCreds.Name = "ci2" + err = p.Credentials.UpsertCredentialSet(context.Background(), testCreds) + require.NoError(t, err, "could not save test credentials (ci2)") return ctx } diff --git a/pkg/porter/install.go b/pkg/porter/install.go index b0478474c..e0614a21d 100644 --- a/pkg/porter/install.go +++ b/pkg/porter/install.go @@ -111,29 +111,14 @@ func (p *Porter) applyActionOptionsToInstallation(ctx context.Context, i *storag return err } - // Record the names of the parameter sets used - i.ParameterSets = append(i.ParameterSets, Unique(i.ParameterSets, opts.ParameterSets...)...) - - // Record the names of the credential sets used - i.CredentialSets = append(i.CredentialSets, Unique(i.CredentialSets, opts.CredentialIdentifiers...)...) - - return nil -} - -func Unique(existings []string, n ...string) []string { - var u []string - old := make(map[string]struct{}) - - for _, e := range existings { - old[e] = struct{}{} + // Record the names of the parameter and credential sets used if specified. Otherwise, reuse the previously specified sets. + // This should replace previously specified sets so that only what was just specified is used. + if len(opts.ParameterSets) > 0 { + i.ParameterSets = opts.ParameterSets } - - for _, cs := range n { - if _, ok := old[cs]; ok { - continue - } - u = append(u, cs) + if len(opts.CredentialIdentifiers) > 0 { + i.CredentialSets = opts.CredentialIdentifiers } - return u + return nil } diff --git a/pkg/porter/install_test.go b/pkg/porter/install_test.go index cdee242ed..5495fca96 100644 --- a/pkg/porter/install_test.go +++ b/pkg/porter/install_test.go @@ -1,9 +1,15 @@ package porter import ( + "context" "testing" + "get.porter.sh/porter/pkg/cnab" "get.porter.sh/porter/pkg/portercontext" + "get.porter.sh/porter/pkg/secrets" + "get.porter.sh/porter/pkg/storage" + "github.com/cnabio/cnab-go/bundle" + "github.com/cnabio/cnab-go/bundle/definition" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -64,24 +70,90 @@ func TestInstallOptions_validateDriver(t *testing.T) { } } -func TestUnique(t *testing.T) { - testcases := []struct { - name string - existing []string - newData []string - expected []string - }{ - {"empty existing data", []string{}, []string{"foo", "bar"}, []string{"foo", "bar"}}, - {"empty new data", []string{"foo", "bar"}, []string{}, nil}, - {"has new data", []string{"foo", "bar"}, []string{"alice"}, []string{"alice"}}, - {"has duplicate new data", []string{"foo", "bar"}, []string{"alice", "foo"}, []string{"alice"}}, - } +func TestPorter_applyActionOptionsToInstallation(t *testing.T) { + setup := func() (context.Context, *TestPorter, *storage.Installation) { + ctx := context.Background() + p := NewTestPorter(t) - for _, tc := range testcases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - result := Unique(tc.existing, tc.newData...) - require.Equal(t, tc.expected, result) + p.TestParameters.InsertParameterSet(ctx, storage.ParameterSet{ + ParameterSetSpec: storage.ParameterSetSpec{ + Name: "newps1", + Parameters: []secrets.Strategy{ + {Name: "logLevel", Source: secrets.Source{Key: "value", Value: "11"}}, + }, + }, }) + + return ctx, p, &storage.Installation{ + Bundle: storage.OCIReferenceParts{ + Repository: "example.com/mybuns", + Version: "1.0.0", + }, + ParameterSets: []string{"oldps1"}, + CredentialSets: []string{"oldcs1", "oldcs2"}, + } } + + t.Run("replace previous sets", func(t *testing.T) { + ctx, p, inst := setup() + + // We should replace the previously used sets since we specified different ones + opts := NewBundleExecutionOptions() + opts.Reference = kahnlatest.String() + opts.bundleRef = &cnab.BundleReference{ + Reference: kahnlatest, + Definition: cnab.NewBundle(bundle.Bundle{ + Credentials: map[string]bundle.Credential{ + "userid": {}, + }, + Parameters: map[string]bundle.Parameter{ + "logLevel": {Definition: "logLevel"}, + }, + Definitions: map[string]*definition.Schema{ + "logLevel": {Type: "string"}, + }, + }), + } + + opts.ParameterSets = []string{"newps1"} + opts.CredentialIdentifiers = []string{"newcs1"} + + require.NoError(t, opts.Validate(ctx, nil, p.Porter)) + err := p.applyActionOptionsToInstallation(ctx, inst, opts) + require.NoError(t, err, "applyActionOptionsToInstallation failed") + + require.Equal(t, opts.ParameterSets, inst.ParameterSets, "expected the installation to replace the credential sets with those specified") + require.Equal(t, opts.CredentialIdentifiers, inst.CredentialSets, "expected the installation to replace the credential sets with those specified") + }) + + t.Run("reuse previous sets", func(t *testing.T) { + ctx, p, inst := setup() + + // We should reuse the previously used sets since we specified different ones + opts := NewBundleExecutionOptions() + opts.Reference = kahnlatest.String() + opts.bundleRef = &cnab.BundleReference{ + Reference: kahnlatest, + Definition: cnab.NewBundle(bundle.Bundle{ + Credentials: map[string]bundle.Credential{ + "userid": {}, + }, + Parameters: map[string]bundle.Parameter{ + "logLevel": {Definition: "logLevel"}, + }, + Definitions: map[string]*definition.Schema{ + "logLevel": {Type: "string"}, + }, + }), + } + opts.ParameterSets = []string{} + opts.CredentialIdentifiers = []string{} + + require.NoError(t, opts.Validate(ctx, nil, p.Porter)) + err := p.applyActionOptionsToInstallation(ctx, inst, opts) + require.NoError(t, err, "applyActionOptionsToInstallation failed") + + require.Equal(t, []string{"oldps1"}, inst.ParameterSets, "expected the installation to reuse the previous credential sets") + require.Equal(t, []string{"oldcs1", "oldcs2"}, inst.CredentialSets, "expected the installation to reuse the previous credential sets") + }) } diff --git a/tests/integration/dependencies_test.go b/tests/integration/dependencies_test.go index 518d0f4b4..aabd03142 100644 --- a/tests/integration/dependencies_test.go +++ b/tests/integration/dependencies_test.go @@ -1,5 +1,4 @@ //go:build integration -// +build integration package integration @@ -66,7 +65,7 @@ func installWordpressBundle(ctx context.Context, p *porter.TestPorter) (namespac namespace = p.RandomString(10) installOpts := porter.NewInstallOptions() installOpts.Namespace = namespace - installOpts.CredentialIdentifiers = []string{"ci"} + installOpts.CredentialIdentifiers = []string{"ci"} // Use the ci credential set, porter should remember this for later installOpts.Params = []string{ "wordpress-password=mypassword", "namespace=" + namespace, @@ -137,7 +136,7 @@ func cleanupWordpressBundle(ctx context.Context, p *porter.TestPorter, namespace func upgradeWordpressBundle(ctx context.Context, p *porter.TestPorter, namespace string) { upgradeOpts := porter.NewUpgradeOptions() upgradeOpts.Namespace = namespace - upgradeOpts.CredentialIdentifiers = []string{"ci"} + // do not specify credential sets, porter should reuse what was specified from install upgradeOpts.Params = []string{ "wordpress-password=mypassword", "namespace=" + namespace, @@ -164,13 +163,18 @@ func upgradeWordpressBundle(ctx context.Context, p *porter.TestPorter, namespace require.NoError(p.T(), err, "GetLastClaim failed") assert.Equal(p.T(), cnab.ActionUpgrade, c.Action, "the root bundle wasn't recorded as being upgraded") assert.Equal(p.T(), cnab.StatusSucceeded, i.Status.ResultStatus, "the root bundle wasn't recorded as being upgraded successfully") + + // Check that we are using the original credential set specified during install + require.Len(p.T(), i.CredentialSets, 1, "expected only one credential set associated to the installation") + assert.Equal(p.T(), "ci", i.CredentialSets[0], "expected to use the alternate credential set") } func invokeWordpressBundle(ctx context.Context, p *porter.TestPorter, namespace string) { invokeOpts := porter.NewInvokeOptions() invokeOpts.Namespace = namespace invokeOpts.Action = "ping" - invokeOpts.CredentialIdentifiers = []string{"ci"} + // Use a different set of creds to run this rando command + invokeOpts.CredentialIdentifiers = []string{"ci2"} invokeOpts.Params = []string{ "wordpress-password=mypassword", "namespace=" + namespace, @@ -196,11 +200,16 @@ func invokeWordpressBundle(ctx context.Context, p *porter.TestPorter, namespace require.NoError(p.T(), err, "GetLastClaim failed") assert.Equal(p.T(), "ping", c.Action, "the root bundle wasn't recorded as being invoked") assert.Equal(p.T(), cnab.StatusSucceeded, i.Status.ResultStatus, "the root bundle wasn't recorded as being invoked successfully") + + // Check that we are now using the alternate credentials with the bundle + require.Len(p.T(), i.CredentialSets, 1, "expected only one credential set associated to the installation") + assert.Equal(p.T(), "ci2", i.CredentialSets[0], "expected to use the alternate credential set") } func uninstallWordpressBundle(ctx context.Context, p *porter.TestPorter, namespace string) { uninstallOptions := porter.NewUninstallOptions() uninstallOptions.Namespace = namespace + // Now go back to using the original set of credentials uninstallOptions.CredentialIdentifiers = []string{"ci"} uninstallOptions.Params = []string{ "namespace=" + namespace, @@ -227,4 +236,9 @@ func uninstallWordpressBundle(ctx context.Context, p *porter.TestPorter, namespa require.NoError(p.T(), err, "GetLastClaim failed") assert.Equal(p.T(), cnab.ActionUninstall, c.Action, "the root bundle wasn't recorded as being uninstalled") assert.Equal(p.T(), cnab.StatusSucceeded, i.Status.ResultStatus, "the root bundle wasn't recorded as being uninstalled successfully") + + // Check that we are now using the original credentials with the bundle + require.Len(p.T(), i.CredentialSets, 1, "expected only one credential set associated to the installation") + assert.Equal(p.T(), "ci", i.CredentialSets[0], "expected to use the alternate credential set") + }