From ea609d546f8d0f3ffe6a1ab7b873da5904f9bc92 Mon Sep 17 00:00:00 2001 From: Fraser Waters Date: Wed, 19 Oct 2022 09:12:25 +0100 Subject: [PATCH] Add 'secret' to config Also separate the validation and merging of project-to-stack values, to allow us to apply in values even if they're secure and we don't have an available decrypter. We can't validate that they're all correct, but it means at least `config get` can do a best effort retrival for config values. --- ...on-of-stack-config-with-secure-values.yaml | 4 + pkg/cmd/pulumi/config.go | 23 ++-- pkg/cmd/pulumi/destroy.go | 7 +- pkg/cmd/pulumi/import.go | 7 +- pkg/cmd/pulumi/logs.go | 7 +- pkg/cmd/pulumi/preview.go | 7 +- pkg/cmd/pulumi/refresh.go | 7 +- pkg/cmd/pulumi/up.go | 14 ++- pkg/cmd/pulumi/watch.go | 7 +- pkg/secrets/b64/manager.go | 25 +---- sdk/go/common/resource/config/crypt.go | 21 ++++ sdk/go/common/resource/config/value.go | 6 +- sdk/go/common/workspace/config.go | 104 +++++++++++++++--- sdk/go/common/workspace/project.go | 25 +++-- sdk/go/common/workspace/project.json | 11 +- sdk/go/common/workspace/project_test.go | 98 ++++++++++++++--- 16 files changed, 283 insertions(+), 90 deletions(-) create mode 100644 changelog/pending/20221019--engine--fix-type-validation-of-stack-config-with-secure-values.yaml diff --git a/changelog/pending/20221019--engine--fix-type-validation-of-stack-config-with-secure-values.yaml b/changelog/pending/20221019--engine--fix-type-validation-of-stack-config-with-secure-values.yaml new file mode 100644 index 000000000000..d36e66f7bb38 --- /dev/null +++ b/changelog/pending/20221019--engine--fix-type-validation-of-stack-config-with-secure-values.yaml @@ -0,0 +1,4 @@ +changes: +- type: fix + scope: engine + description: Fix type validation of stack config with secure values. diff --git a/pkg/cmd/pulumi/config.go b/pkg/cmd/pulumi/config.go index 507a1976c6b2..c9b5f797a58e 100644 --- a/pkg/cmd/pulumi/config.go +++ b/pkg/cmd/pulumi/config.go @@ -794,9 +794,9 @@ func listConfig(ctx context.Context, stackName := stack.Ref().Name().String() // when listing configuration values // also show values coming from the project - configError := workspace.ValidateStackConfigAndApplyProjectConfig(stackName, project, ps.Config) - if configError != nil { - return configError + err = workspace.ApplyProjectConfig(stackName, project, ps.Config) + if err != nil { + return err } cfg := ps.Config @@ -804,11 +804,11 @@ func listConfig(ctx context.Context, // By default, we will use a blinding decrypter to show "[secret]". If requested, display secrets in plaintext. decrypter := config.NewBlindingDecrypter() if cfg.HasSecureValue() && showSecrets { - dec, decerr := getStackDecrypter(stack) - if decerr != nil { - return decerr + stackDecrypter, err := getStackDecrypter(stack) + if err != nil { + return err } - decrypter = dec + decrypter = stackDecrypter } var keys config.KeyArray @@ -887,12 +887,13 @@ func getConfig(ctx context.Context, stack backend.Stack, key config.Key, path, j if err != nil { return err } + stackName := stack.Ref().Name().String() - configError := workspace.ValidateStackConfigAndApplyProjectConfig(stackName, project, ps.Config) - if configError != nil { - return configError + // when asking for a configuration value, include values from the project config + err = workspace.ApplyProjectConfig(stackName, project, ps.Config) + if err != nil { + return err } - cfg := ps.Config v, ok, err := cfg.Get(key, path) diff --git a/pkg/cmd/pulumi/destroy.go b/pkg/cmd/pulumi/destroy.go index 38b5385e3842..0c746cfdae19 100644 --- a/pkg/cmd/pulumi/destroy.go +++ b/pkg/cmd/pulumi/destroy.go @@ -165,8 +165,13 @@ func newDestroyCmd() *cobra.Command { return result.FromError(fmt.Errorf("getting stack configuration: %w", err)) } + decrypter, err := sm.Decrypter() + if err != nil { + return result.FromError(fmt.Errorf("getting stack decrypter: %w", err)) + } + stackName := s.Ref().Name().String() - configError := workspace.ValidateStackConfigAndApplyProjectConfig(stackName, proj, cfg.Config) + configError := workspace.ValidateStackConfigAndApplyProjectConfig(stackName, proj, cfg.Config, decrypter) if configError != nil { return result.FromError(fmt.Errorf("validating stack config: %w", configError)) } diff --git a/pkg/cmd/pulumi/import.go b/pkg/cmd/pulumi/import.go index d4015cea3715..5dea64c51398 100644 --- a/pkg/cmd/pulumi/import.go +++ b/pkg/cmd/pulumi/import.go @@ -498,7 +498,12 @@ func newImportCmd() *cobra.Command { return result.FromError(fmt.Errorf("getting stack configuration: %w", err)) } - configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config) + decrypter, err := sm.Decrypter() + if err != nil { + return result.FromError(fmt.Errorf("getting stack decrypter: %w", err)) + } + + configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter) if configErr != nil { return result.FromError(fmt.Errorf("validating stack config: %w", configErr)) } diff --git a/pkg/cmd/pulumi/logs.go b/pkg/cmd/pulumi/logs.go index 973fd85e2806..e2aead4a174f 100644 --- a/pkg/cmd/pulumi/logs.go +++ b/pkg/cmd/pulumi/logs.go @@ -79,7 +79,12 @@ func newLogsCmd() *cobra.Command { return fmt.Errorf("getting stack configuration: %w", err) } - configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config) + decrypter, err := sm.Decrypter() + if err != nil { + return fmt.Errorf("getting stack decrypter: %w", err) + } + + configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter) if configErr != nil { return fmt.Errorf("validating stack config: %w", configErr) } diff --git a/pkg/cmd/pulumi/preview.go b/pkg/cmd/pulumi/preview.go index c8ebc922279b..96609fafd019 100644 --- a/pkg/cmd/pulumi/preview.go +++ b/pkg/cmd/pulumi/preview.go @@ -154,7 +154,12 @@ func newPreviewCmd() *cobra.Command { return result.FromError(fmt.Errorf("getting stack configuration: %w", err)) } - configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config) + decrypter, err := sm.Decrypter() + if err != nil { + return result.FromError(fmt.Errorf("getting stack decrypter: %w", err)) + } + + configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter) if configErr != nil { return result.FromError(fmt.Errorf("validating stack config: %w", configErr)) } diff --git a/pkg/cmd/pulumi/refresh.go b/pkg/cmd/pulumi/refresh.go index eb1ecd3b32a2..ac8052089128 100644 --- a/pkg/cmd/pulumi/refresh.go +++ b/pkg/cmd/pulumi/refresh.go @@ -152,7 +152,12 @@ func newRefreshCmd() *cobra.Command { return result.FromError(fmt.Errorf("getting stack configuration: %w", err)) } - configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config) + decrypter, err := sm.Decrypter() + if err != nil { + return result.FromError(fmt.Errorf("getting stack decrypter: %w", err)) + } + + configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter) if configErr != nil { return result.FromError(fmt.Errorf("validating stack config: %w", configErr)) } diff --git a/pkg/cmd/pulumi/up.go b/pkg/cmd/pulumi/up.go index 0d19fb5adf57..5b573c256f6f 100644 --- a/pkg/cmd/pulumi/up.go +++ b/pkg/cmd/pulumi/up.go @@ -111,7 +111,12 @@ func newUpCmd() *cobra.Command { return result.FromError(fmt.Errorf("getting stack configuration: %w", err)) } - configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config) + decrypter, err := sm.Decrypter() + if err != nil { + return result.FromError(fmt.Errorf("getting stack decrypter: %w", err)) + } + + configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter) if configErr != nil { return result.FromError(fmt.Errorf("validating stack config: %w", configErr)) } @@ -350,7 +355,12 @@ func newUpCmd() *cobra.Command { return result.FromError(fmt.Errorf("getting stack configuration: %w", err)) } - configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config) + decrypter, err := sm.Decrypter() + if err != nil { + return result.FromError(fmt.Errorf("getting stack decrypter: %w", err)) + } + + configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter) if configErr != nil { return result.FromError(fmt.Errorf("validating stack config: %w", configErr)) } diff --git a/pkg/cmd/pulumi/watch.go b/pkg/cmd/pulumi/watch.go index 46a6a3edc193..eeb1df36ace8 100644 --- a/pkg/cmd/pulumi/watch.go +++ b/pkg/cmd/pulumi/watch.go @@ -117,7 +117,12 @@ func newWatchCmd() *cobra.Command { return result.FromError(fmt.Errorf("getting stack configuration: %w", err)) } - configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config) + decrypter, err := sm.Decrypter() + if err != nil { + return result.FromError(fmt.Errorf("getting stack decrypter: %w", err)) + } + + configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter) if configErr != nil { return result.FromError(fmt.Errorf("validating stack config: %w", configErr)) } diff --git a/pkg/secrets/b64/manager.go b/pkg/secrets/b64/manager.go index cfe5d341d5ce..bae5dd63dd77 100644 --- a/pkg/secrets/b64/manager.go +++ b/pkg/secrets/b64/manager.go @@ -16,9 +16,6 @@ package b64 import ( - "context" - "encoding/base64" - "github.com/pulumi/pulumi/pkg/v3/secrets" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/config" ) @@ -34,23 +31,5 @@ type manager struct{} func (m *manager) Type() string { return Type } func (m *manager) State() interface{} { return map[string]string{} } -func (m *manager) Encrypter() (config.Encrypter, error) { return &base64Crypter{}, nil } -func (m *manager) Decrypter() (config.Decrypter, error) { return &base64Crypter{}, nil } - -type base64Crypter struct{} - -func (c *base64Crypter) EncryptValue(ctx context.Context, s string) (string, error) { - return base64.StdEncoding.EncodeToString([]byte(s)), nil -} - -func (c *base64Crypter) DecryptValue(ctx context.Context, s string) (string, error) { - b, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return "", err - } - return string(b), nil -} - -func (c *base64Crypter) BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error) { - return config.DefaultBulkDecrypt(ctx, c, ciphertexts) -} +func (m *manager) Encrypter() (config.Encrypter, error) { return config.Base64Crypter, nil } +func (m *manager) Decrypter() (config.Decrypter, error) { return config.Base64Crypter, nil } diff --git a/sdk/go/common/resource/config/crypt.go b/sdk/go/common/resource/config/crypt.go index adc3845f4133..96ae233da776 100644 --- a/sdk/go/common/resource/config/crypt.go +++ b/sdk/go/common/resource/config/crypt.go @@ -275,3 +275,24 @@ func DefaultBulkDecrypt(ctx context.Context, } return secretMap, nil } + +type base64Crypter struct{} + +// Base64Crypter is a Crypter that "encrypts" by encoding the string to base64. +var Base64Crypter Crypter = &base64Crypter{} + +func (c *base64Crypter) EncryptValue(ctx context.Context, s string) (string, error) { + return base64.StdEncoding.EncodeToString([]byte(s)), nil +} + +func (c *base64Crypter) DecryptValue(ctx context.Context, s string) (string, error) { + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "", err + } + return string(b), nil +} + +func (c *base64Crypter) BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error) { + return DefaultBulkDecrypt(ctx, c, ciphertexts) +} diff --git a/sdk/go/common/resource/config/value.go b/sdk/go/common/resource/config/value.go index 613990013fec..e6d19d6fee8e 100644 --- a/sdk/go/common/resource/config/value.go +++ b/sdk/go/common/resource/config/value.go @@ -140,7 +140,7 @@ func (c Value) ToObject() (interface{}, error) { } func (c Value) MarshalJSON() ([]byte, error) { - v, err := c.MarshalValue() + v, err := c.marshalValue() if err != nil { return nil, err } @@ -158,7 +158,7 @@ func (c *Value) UnmarshalJSON(b []byte) error { } func (c Value) MarshalYAML() (interface{}, error) { - return c.MarshalValue() + return c.marshalValue() } func (c *Value) UnmarshalYAML(unmarshal func(interface{}) error) error { @@ -202,7 +202,7 @@ func (c *Value) unmarshalValue(unmarshal func(interface{}) error, fix func(inter return nil } -func (c Value) MarshalValue() (interface{}, error) { +func (c Value) marshalValue() (interface{}, error) { if c.object { return c.unmarshalObjectJSON() } diff --git a/sdk/go/common/workspace/config.go b/sdk/go/common/workspace/config.go index 06a157bdce05..e51b289f1617 100644 --- a/sdk/go/common/workspace/config.go +++ b/sdk/go/common/workspace/config.go @@ -64,10 +64,59 @@ func missingProjectConfigurationKeysError(missingProjectKeys []string, stackName isOrAre) } -func ValidateStackConfigAndApplyProjectConfig( +type StackName = string +type ProjectConfigKey = string +type StackConfigValidator = func(StackName, ProjectConfigKey, ProjectConfigType, config.Value, config.Decrypter) error + +func DefaultStackConfigValidator( + stackName string, + projectConfigKey string, + projectConfigType ProjectConfigType, + stackValue config.Value, + dec config.Decrypter) error { + // First check if the project says this should be secret, and if so that the stack value is + // secure. + if projectConfigType.Secret && !stackValue.Secure() { + validationError := fmt.Errorf( + "Stack '%v' with configuration key '%v' must be encrypted as it's secret", + stackName, + projectConfigKey) + return validationError + } + + value, err := stackValue.Value(dec) + if err != nil { + return err + } + // Content will be a JSON string if object is true, so marshal that back into an actual structure + var content interface{} = value + if stackValue.Object() { + err = json.Unmarshal([]byte(value), &content) + if err != nil { + return err + } + } + + if !ValidateConfigValue(projectConfigType.Type, projectConfigType.Items, content) { + typeName := InferFullTypeName(projectConfigType.Type, projectConfigType.Items) + validationError := fmt.Errorf( + "Stack '%v' with configuration key '%v' must be of type '%v'", + stackName, + projectConfigKey, + typeName) + + return validationError + } + + return nil +} + +func ValidateStackConfigAndMergeProjectConfig( stackName string, project *Project, - stackConfig config.Map) error { + stackConfig config.Map, + lazyDecrypter func() config.Decrypter, + validate StackConfigValidator) error { if len(project.Config) > 0 { // only when the project defines config values, do we need to validate the stack config @@ -90,6 +139,7 @@ func ValidateStackConfigAndApplyProjectConfig( } } + var decrypter config.Decrypter missingConfigurationKeys := make([]string, 0) for projectConfigKey, projectConfigType := range project.Config { var key config.Key @@ -143,23 +193,20 @@ func ValidateStackConfigAndApplyProjectConfig( return setError } } else { - // found value on the stack level - // retrieve it and validate it against - // the config defined at the project level - content, contentError := stackValue.MarshalValue() - if contentError != nil { - return contentError - } - - if !ValidateConfigValue(projectConfigType.Type, projectConfigType.Items, content) { - typeName := InferFullTypeName(projectConfigType.Type, projectConfigType.Items) - validationError := fmt.Errorf( - "Stack '%v' with configuration key '%v' must be of type '%v'", - stackName, - projectConfigKey, - typeName) + // Validate stack level value against the config defined at the project level + if validate != nil { + // we have a validator + if decrypter == nil && lazyDecrypter != nil { + // initialize the decrypter once + decrypter = lazyDecrypter() + } - return validationError + if decrypter != nil { + validationError := validate(stackName, projectConfigKey, projectConfigType, stackValue, decrypter) + if validationError != nil { + return validationError + } + } } } } @@ -172,3 +219,24 @@ func ValidateStackConfigAndApplyProjectConfig( return nil } + +func ValidateStackConfigAndApplyProjectConfig( + stackName string, + project *Project, + stackConfig config.Map, + dec config.Decrypter) error { + decrypter := func() config.Decrypter { + return dec + } + + return ValidateStackConfigAndMergeProjectConfig( + stackName, project, stackConfig, decrypter, DefaultStackConfigValidator) +} + +// ApplyConfigDefaults applies the default values for the project configuration onto the stack configuration +// without validating the contents of stack config values. +// This is because sometimes during pulumi config ls and pulumi config get, if users are +// using PassphraseDecrypter, we don't want to always prompt for the values when not necessary +func ApplyProjectConfig(stackName string, project *Project, stackConfig config.Map) error { + return ValidateStackConfigAndMergeProjectConfig(stackName, project, stackConfig, nil, nil) +} diff --git a/sdk/go/common/workspace/project.go b/sdk/go/common/workspace/project.go index 7f417382a951..94dc06682641 100644 --- a/sdk/go/common/workspace/project.go +++ b/sdk/go/common/workspace/project.go @@ -19,6 +19,7 @@ import ( "encoding/json" "fmt" "io" + "math" "os" "path/filepath" "strconv" @@ -107,6 +108,7 @@ type ProjectConfigType struct { Description string `json:"description,omitempty" yaml:"description,omitempty"` Items *ProjectConfigItemsType `json:"items,omitempty" yaml:"items,omitempty"` Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` + Secret bool `json:"secret,omitempty" yaml:"secret,omitempty"` } // Project is a Pulumi project manifest. @@ -345,14 +347,23 @@ func ValidateConfigValue(typeName string, itemsType *ProjectConfigItemsType, val if typeName == "integer" { _, ok := value.(int) - if !ok { - valueAsText, isText := value.(string) - if isText { - _, integerParseError := strconv.Atoi(valueAsText) - return integerParseError == nil - } + if ok { + return true } - return ok + // Config values come from YAML which by default will return floats not int. If it's a whole number + // we'll allow it here though + f, ok := value.(float64) + if ok && f == math.Trunc(f) { + return true + } + // Allow strings here if they parse as integers + valueAsText, isText := value.(string) + if isText { + _, integerParseError := strconv.Atoi(valueAsText) + return integerParseError == nil + } + + return false } if typeName == "boolean" { diff --git a/sdk/go/common/workspace/project.json b/sdk/go/common/workspace/project.json index c1aa08d8e427..f1b1d137e0b3 100644 --- a/sdk/go/common/workspace/project.json +++ b/sdk/go/common/workspace/project.json @@ -78,6 +78,13 @@ "string", "null" ], + "properties":{ + "secret":{ + "description":"If true this configuration value should be encrypted.", + "type":"boolean", + "default":false + } + }, "additionalProperties":{ "oneOf":[ { @@ -317,9 +324,7 @@ "secret":{ "type":"boolean" }, - "default":{ - - } + "default":{ } } } } diff --git a/sdk/go/common/workspace/project_test.go b/sdk/go/common/workspace/project_test.go index 3e20b9c35cbe..00d685b2d5be 100644 --- a/sdk/go/common/workspace/project_test.go +++ b/sdk/go/common/workspace/project_test.go @@ -1,7 +1,9 @@ package workspace import ( + "context" "encoding/json" + "fmt" "io/ioutil" "os" "testing" @@ -264,48 +266,58 @@ config: type: array items: type: string + secretString: + type: string + secret: true ` project, err := loadProjectFromText(t, projectContent) assert.NoError(t, err, "Should be able to load the project") - assert.Equal(t, 8, len(project.Config), "There are 8 config type definition") + assert.Equal(t, 9, len(project.Config), "There are 9 config type definition") // full integer config schema integerSchemFull, ok := project.Config["integerSchemaFull"] assert.True(t, ok, "should be able to read integerSchemaFull") assert.Equal(t, "integer", integerSchemFull.Type) assert.Equal(t, "a very important value", integerSchemFull.Description) assert.Equal(t, 1, integerSchemFull.Default) + assert.False(t, integerSchemFull.Secret) assert.Nil(t, integerSchemFull.Items, "Primtive config type doesn't have an items type") integerSchemaSimple, ok := project.Config["integerSchemaSimple"] assert.True(t, ok, "should be able to read integerSchemaSimple") assert.Equal(t, "integer", integerSchemaSimple.Type, "integer type is inferred correctly") + assert.False(t, integerSchemaSimple.Secret) assert.Equal(t, 20, integerSchemaSimple.Default, "Default integer value is parsed correctly") textSchemaFull, ok := project.Config["textSchemaFull"] assert.True(t, ok, "should be able to read textSchemaFull") assert.Equal(t, "string", textSchemaFull.Type) + assert.False(t, textSchemaFull.Secret) assert.Equal(t, "t3.micro", textSchemaFull.Default) assert.Equal(t, "", textSchemaFull.Description) textSchemaSimple, ok := project.Config["textSchemaSimple"] assert.True(t, ok, "should be able to read textSchemaSimple") assert.Equal(t, "string", textSchemaSimple.Type) + assert.False(t, textSchemaSimple.Secret) assert.Equal(t, "t4.large", textSchemaSimple.Default) booleanSchemaFull, ok := project.Config["booleanSchemaFull"] assert.True(t, ok, "should be able to read booleanSchemaFull") assert.Equal(t, "boolean", booleanSchemaFull.Type) + assert.False(t, booleanSchemaFull.Secret) assert.Equal(t, true, booleanSchemaFull.Default) booleanSchemaSimple, ok := project.Config["booleanSchemaSimple"] assert.True(t, ok, "should be able to read booleanSchemaSimple") assert.Equal(t, "boolean", booleanSchemaSimple.Type) + assert.False(t, booleanSchemaSimple.Secret) assert.Equal(t, false, booleanSchemaSimple.Default) simpleArrayOfStrings, ok := project.Config["simpleArrayOfStrings"] assert.True(t, ok, "should be able to read simpleArrayOfStrings") assert.Equal(t, "array", simpleArrayOfStrings.Type) + assert.False(t, simpleArrayOfStrings.Secret) assert.NotNil(t, simpleArrayOfStrings.Items) assert.Equal(t, "string", simpleArrayOfStrings.Items.Type) arrayValues := simpleArrayOfStrings.Default.([]interface{}) @@ -314,10 +326,19 @@ config: arrayOfArrays, ok := project.Config["arrayOfArrays"] assert.True(t, ok, "should be able to read arrayOfArrays") assert.Equal(t, "array", arrayOfArrays.Type) + assert.False(t, arrayOfArrays.Secret) assert.NotNil(t, arrayOfArrays.Items) assert.Equal(t, "array", arrayOfArrays.Items.Type) assert.NotNil(t, arrayOfArrays.Items.Items) assert.Equal(t, "string", arrayOfArrays.Items.Items.Type) + + secretString, ok := project.Config["secretString"] + assert.True(t, ok, "should be able to read secretString") + assert.Equal(t, "string", secretString.Type) + assert.Equal(t, "", secretString.Description) + assert.Equal(t, nil, secretString.Default) + assert.True(t, secretString.Secret) + assert.Nil(t, secretString.Items) } func getConfigValue(t *testing.T, stackConfig config.Map, key string) string { @@ -348,7 +369,7 @@ config: assert.NoError(t, projectError, "Shold be able to load the project") stack, stackError := loadProjectStackFromText(t, project, projectStackYaml) assert.NoError(t, stackError, "Should be able to read the stack") - configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config) + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, config.NewPanicCrypter()) assert.NoError(t, configError, "Config override should be valid") assert.Equal(t, 3, len(stack.Config), "Stack config now has three values") @@ -376,7 +397,7 @@ config: assert.NoError(t, projectError, "Shold be able to load the project") stack, stackError := loadProjectStackFromText(t, project, projectStackYaml) assert.NoError(t, stackError, "Should be able to read the stack") - configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config) + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, config.NewPanicCrypter()) assert.NoError(t, configError, "Config override should be valid") assert.Equal(t, 2, len(stack.Config), "Stack config now has three values") @@ -403,7 +424,7 @@ config: assert.NoError(t, projectError, "Shold be able to load the project") stack, stackError := loadProjectStackFromText(t, project, projectStackYaml) assert.NoError(t, stackError, "Should be able to read the stack") - configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config) + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, config.NewPanicCrypter()) assert.NoError(t, configError, "Config override should be valid") assert.Equal(t, 2, len(stack.Config), "Stack config now has three values") @@ -421,7 +442,7 @@ runtime: dotnet config: values: type: array - items: + items: type: string default: [value]` @@ -434,7 +455,7 @@ config: assert.NoError(t, projectError, "Shold be able to load the project") stack, stackError := loadProjectStackFromText(t, project, projectStackYaml) assert.NoError(t, stackError, "Should be able to read the stack") - configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config) + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, config.NewPanicCrypter()) assert.NotNil(t, configError, "there should be a config type error") assert.Contains(t, configError.Error(), "Stack 'dev' with configuration key 'values' must be of type 'array'") } @@ -507,12 +528,13 @@ config: assert.NoError(t, projectError, "Shold be able to load the project") stack, stackError := loadProjectStackFromText(t, project, projectStackYamlValid) assert.NoError(t, stackError, "Should be able to read the stack") - configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config) + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, config.NewPanicCrypter()) assert.NoError(t, configError, "there should no config type error") invalidStackConfig, stackError := loadProjectStackFromText(t, project, projectStackYamlInvalid) assert.NoError(t, stackError, "Should be able to read the stack") - configError = ValidateStackConfigAndApplyProjectConfig("dev", project, invalidStackConfig.Config) + configError = ValidateStackConfigAndApplyProjectConfig( + "dev", project, invalidStackConfig.Config, config.NewPanicCrypter()) assert.NotNil(t, configError, "there should be a config type error") assert.Contains(t, configError.Error(), @@ -527,7 +549,7 @@ runtime: dotnet config: values: type: array - items: + items: type: string` projectStackYaml := `` @@ -536,7 +558,7 @@ config: assert.NoError(t, projectError, "Shold be able to load the project") stack, stackError := loadProjectStackFromText(t, project, projectStackYaml) assert.NoError(t, stackError, "Should be able to read the stack") - configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config) + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, config.NewPanicCrypter()) assert.NotNil(t, configError, "there should be a config type error") assert.Contains(t, configError.Error(), "Stack 'dev' is missing configuration value 'values'") } @@ -551,7 +573,7 @@ config: type: string values: type: array - items: + items: type: string` projectStackYaml := `` @@ -560,7 +582,7 @@ config: assert.NoError(t, projectError, "Shold be able to load the project") stack, stackError := loadProjectStackFromText(t, project, projectStackYaml) assert.NoError(t, stackError, "Should be able to read the stack") - configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config) + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, config.NewPanicCrypter()) assert.NotNil(t, configError, "there should be a config type error") assert.Contains(t, configError.Error(), "Stack 'dev' is missing configuration values 'another' and 'values'") } @@ -575,7 +597,7 @@ config: type: integer values: type: array - items: + items: type: string world: type: string` @@ -586,7 +608,7 @@ config: assert.NoError(t, projectError, "Shold be able to load the project") stack, stackError := loadProjectStackFromText(t, project, projectStackYaml) assert.NoError(t, stackError, "Should be able to read the stack") - configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config) + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, config.NewPanicCrypter()) assert.NotNil(t, configError, "there should be a config type error") assert.Contains(t, configError.Error(), "Stack 'dev' is missing configuration values 'hello', 'values' and 'world'") } @@ -609,7 +631,7 @@ config: assert.NoError(t, projectError, "Shold be able to load the project") stack, stackError := loadProjectStackFromText(t, project, projectStackYaml) assert.NoError(t, stackError, "Should be able to read the stack") - configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config) + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, config.NewPanicCrypter()) assert.NotNil(t, configError, "there should be a config type error") expectedErrorMsg := "Stack 'dev' uses configuration value 'world' which is not defined by the project configuration" assert.Contains(t, configError.Error(), expectedErrorMsg) @@ -634,7 +656,7 @@ config: assert.NoError(t, projectError, "Shold be able to load the project") stack, stackError := loadProjectStackFromText(t, project, projectStackYaml) assert.NoError(t, stackError, "Should be able to read the stack") - configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config) + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, config.NewPanicCrypter()) assert.NotNil(t, configError, "there should be a config type error") expectedErrorMsg := "Stack 'dev' uses configuration values 'another' and 'world'" + " which are not defined by the project configuration" @@ -657,10 +679,52 @@ config: assert.NoError(t, projectError, "Shold be able to load the project") stack, stackError := loadProjectStackFromText(t, project, projectStackYaml) assert.NoError(t, stackError, "Should be able to read the stack") - configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config) + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, config.NewPanicCrypter()) assert.Nil(t, configError, "there should not be a config type error") } +func TestStackConfigSecretIsCorrectlyValidated(t *testing.T) { + t.Parallel() + projectYaml := ` +name: test +runtime: dotnet +config: + importantNumber: + type: integer + secret: true +` + + crypter := config.Base64Crypter + encryptedValue, err := crypter.EncryptValue(context.Background(), "20") + assert.NoError(t, err) + + projectStackYamlValid := fmt.Sprintf(` +config: + test:importantNumber: + secure: %s +`, encryptedValue) + + projectStackYamlInvalid := ` +config: + test:importantNumber: 20 +` + + project, projectError := loadProjectFromText(t, projectYaml) + assert.NoError(t, projectError, "Shold be able to load the project") + stack, stackError := loadProjectStackFromText(t, project, projectStackYamlValid) + assert.NoError(t, stackError, "Should be able to read the stack") + configError := ValidateStackConfigAndApplyProjectConfig("dev", project, stack.Config, crypter) + assert.NoError(t, configError, "there should no config type error") + + invalidStackConfig, stackError := loadProjectStackFromText(t, project, projectStackYamlInvalid) + assert.NoError(t, stackError, "Should be able to read the stack") + configError = ValidateStackConfigAndApplyProjectConfig("dev", project, invalidStackConfig.Config, crypter) + assert.NotNil(t, configError, "there should be a config type error") + assert.Contains(t, + configError.Error(), + "Stack 'dev' with configuration key 'importantNumber' must be encrypted as it's secret") +} + func TestProjectLoadYAML(t *testing.T) { t.Parallel()