From 77b19a8e0626e5f609f4f4f39978189fdb0f1da1 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Wed, 17 Feb 2021 15:46:38 -0600 Subject: [PATCH] Support Credential.ApplyTo Bundles may set the applyTo field on credentials. When it is set, only require the credential when it applies to the specified action. Implements https://github.com/cnabio/cnab-spec/pull/407 Signed-off-by: Carolyn Van Slyck --- bundle/credentials.go | 16 ++++++++++-- bundle/credentials_test.go | 6 +++-- bundle/outputs.go | 17 ++++++------- bundle/parameters.go | 15 +++++------ bundle/scoped.go | 22 ++++++++++++++++ bundle/scoped_test.go | 41 ++++++++++++++++++++++++++++++ credentials/credentialset.go | 7 +++++- credentials/credentialset_test.go | 42 +++++++++++++++++++++++++++++++ schema/schema/claim.schema.json | 2 +- 9 files changed, 143 insertions(+), 25 deletions(-) create mode 100644 bundle/scoped.go create mode 100644 bundle/scoped_test.go diff --git a/bundle/credentials.go b/bundle/credentials.go index 7efd19d9..dd5c5e71 100644 --- a/bundle/credentials.go +++ b/bundle/credentials.go @@ -5,8 +5,20 @@ import "errors" // Credential represents the definition of a CNAB credential type Credential struct { Location `yaml:",inline"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + ApplyTo []string `json:"applyTo,omitempty" yaml:"applyTo,omitempty"` +} + +// GetApplyTo returns the list of actions that the Credential applies to. +func (c *Credential) GetApplyTo() []string { + return c.ApplyTo +} + +// AppliesTo returns a boolean value specifying whether or not +// the Credential applies to the provided action +func (c *Credential) AppliesTo(action string) bool { + return AppliesTo(c, action) } // Validate a Credential diff --git a/bundle/credentials_test.go b/bundle/credentials_test.go index 03566d67..cd7bc570 100644 --- a/bundle/credentials_test.go +++ b/bundle/credentials_test.go @@ -13,7 +13,8 @@ func TestCompleteCredDefinition(t *testing.T) { "something": { "description" : "wicked this way comes", "path" : "/cnab/app/a/credential", - "required" : true + "required" : true, + "applyTo" : ["install"] } } }` @@ -26,7 +27,8 @@ func TestCompleteCredDefinition(t *testing.T) { assert.Equal(t, "/cnab/app/a/credential", something.Path, "did not contain the expected path") assert.Equal(t, "wicked this way comes", something.Description, "did not contain the expected description") - + assert.True(t, something.Required, "did not contain the expected required") + assert.Equal(t, []string{"install"}, something.ApplyTo, "did not contain the expected applyTo") } func TestHandleMultipleCreds(t *testing.T) { diff --git a/bundle/outputs.go b/bundle/outputs.go index 2ec03fd1..32b292b7 100644 --- a/bundle/outputs.go +++ b/bundle/outputs.go @@ -11,18 +11,15 @@ type Output struct { Path string `json:"path" yaml:"path"` } +// GetApplyTo returns the list of actions that the Output applies to. +func (o Output) GetApplyTo() []string { + return o.ApplyTo +} + // AppliesTo returns a boolean value specifying whether or not // the Output applies to the provided action -func (output *Output) AppliesTo(action string) bool { - if len(output.ApplyTo) == 0 { - return true - } - for _, act := range output.ApplyTo { - if action == act { - return true - } - } - return false +func (o Output) AppliesTo(action string) bool { + return AppliesTo(o, action) } // IsOutputSensitive is a convenience function that determines if an output's diff --git a/bundle/parameters.go b/bundle/parameters.go index 3cf1e1d5..c9a13dd5 100644 --- a/bundle/parameters.go +++ b/bundle/parameters.go @@ -11,18 +11,15 @@ type Parameter struct { Required bool `json:"required,omitempty" yaml:"required,omitempty"` } +// GetApplyTo returns the list of actions that the Parameter applies to. +func (p *Parameter) GetApplyTo() []string { + return p.ApplyTo +} + // AppliesTo returns a boolean value specifying whether or not // the Parameter applies to the provided action func (p *Parameter) AppliesTo(action string) bool { - if len(p.ApplyTo) == 0 { - return true - } - for _, act := range p.ApplyTo { - if action == act { - return true - } - } - return false + return AppliesTo(p, action) } // Validate a Parameter diff --git a/bundle/scoped.go b/bundle/scoped.go new file mode 100644 index 00000000..16a361fd --- /dev/null +++ b/bundle/scoped.go @@ -0,0 +1,22 @@ +package bundle + +// Scoped represents an item whose scope is limited to a set of actions. +type Scoped interface { + // GetApplyTo returns the list of applicable actions. + GetApplyTo() []string +} + +// AppliesTo returns a boolean value specifying whether or not +// the scoped item applies to the provided action. +func AppliesTo(s Scoped, action string) bool { + applyTo := s.GetApplyTo() + if len(applyTo) == 0 { + return true + } + for _, act := range applyTo { + if action == act { + return true + } + } + return false +} diff --git a/bundle/scoped_test.go b/bundle/scoped_test.go new file mode 100644 index 00000000..48af954a --- /dev/null +++ b/bundle/scoped_test.go @@ -0,0 +1,41 @@ +package bundle + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAppliesTo(t *testing.T) { + makeTestTypes := func(applyTo []string) []Scoped { + return []Scoped{ + &Credential{ApplyTo: applyTo}, + Output{ApplyTo: applyTo}, + &Parameter{ApplyTo: applyTo}, + } + } + + t.Run("empty", func(t *testing.T) { + testTypes := makeTestTypes(nil) + + for _, tt := range testTypes { + assert.True(t, AppliesTo(tt, "install"), "%T.AppliesTo returned an incorrect result", tt) + } + }) + + t.Run("hit", func(t *testing.T) { + testTypes := makeTestTypes([]string{"install", "upgrade", "custom"}) + + for _, tt := range testTypes { + assert.True(t, AppliesTo(tt, "custom"), "%T.AppliesTo returned an incorrect result", tt) + } + }) + + t.Run("miss", func(t *testing.T) { + testTypes := makeTestTypes([]string{"install", "upgrade", "uninstall"}) + + for _, tt := range testTypes { + assert.False(t, AppliesTo(tt, "custom"), "%T.AppliesTo returned an incorrect result", tt) + } + }) +} diff --git a/credentials/credentialset.go b/credentials/credentialset.go index 1ddbb322..0355666d 100644 --- a/credentials/credentialset.go +++ b/credentials/credentialset.go @@ -70,11 +70,16 @@ func Load(path string) (*CredentialSet, error) { // This will result in an error only when the following conditions are true: // - a credential in the spec is not present in the given set // - the credential is required +// - the credential applies to the specified action // // It is allowed for spec to specify both an env var and a file. In such case, if // the given set provides either, it will be considered valid. -func Validate(given valuesource.Set, spec map[string]bundle.Credential) error { +func Validate(given valuesource.Set, spec map[string]bundle.Credential, action string) error { for name, cred := range spec { + if !cred.AppliesTo(action) { + continue + } + if !valuesource.IsValid(given, name) && cred.Required { return fmt.Errorf("bundle requires credential for %s", name) } diff --git a/credentials/credentialset_test.go b/credentials/credentialset_test.go index 83c43e9d..dc8331ba 100644 --- a/credentials/credentialset_test.go +++ b/credentials/credentialset_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/cnabio/cnab-go/bundle" "github.com/cnabio/cnab-go/schema" "github.com/cnabio/cnab-go/secrets/host" "github.com/cnabio/cnab-go/valuesource" @@ -77,3 +78,44 @@ func TestNewCredentialSet(t *testing.T) { assert.Equal(t, DefaultSchemaVersion, cs.SchemaVersion, "SchemaVersion was not set") assert.Len(t, cs.Credentials, 1, "Credentials should be initialized with 1 value") } + +func TestValidate(t *testing.T) { + t.Run("valid - credential specified", func(t *testing.T) { + spec := map[string]bundle.Credential{ + "kubeconfig": {}, + } + values := valuesource.Set{ + "kubeconfig": "top secret creds", + } + err := Validate(values, spec, "install") + require.NoError(t, err, "expected Validate to pass because the credential was specified") + }) + + t.Run("valid - credential not required", func(t *testing.T) { + spec := map[string]bundle.Credential{ + "kubeconfig": {ApplyTo: []string{"install"}, Required: false}, + } + values := valuesource.Set{} + err := Validate(values, spec, "install") + require.NoError(t, err, "expected Validate to pass because the credential isn't required") + }) + + t.Run("valid - missing inapplicable credential", func(t *testing.T) { + spec := map[string]bundle.Credential{ + "kubeconfig": {ApplyTo: []string{"install"}, Required: true}, + } + values := valuesource.Set{} + err := Validate(values, spec, "custom") + require.NoError(t, err, "expected Validate to pass because the credential isn't applicable to the custom action") + }) + + t.Run("invalid - missing required credential", func(t *testing.T) { + spec := map[string]bundle.Credential{ + "kubeconfig": {ApplyTo: []string{"install"}, Required: true}, + } + values := valuesource.Set{} + err := Validate(values, spec, "install") + require.Error(t, err, "expected Validate to fail because the credential applies to the specified action and is required") + assert.Contains(t, err.Error(), "bundle requires credential") + }) +} diff --git a/schema/schema/claim.schema.json b/schema/schema/claim.schema.json index ba589212..102b5357 100644 --- a/schema/schema/claim.schema.json +++ b/schema/schema/claim.schema.json @@ -95,4 +95,4 @@ ], "title": "CNAB Claims json schema", "type": "object" -} \ No newline at end of file +}