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..c741513c7a5f 100644 --- a/pkg/cmd/pulumi/config.go +++ b/pkg/cmd/pulumi/config.go @@ -791,24 +791,16 @@ func listConfig(ctx context.Context, return err } - 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 - } - cfg := ps.Config // 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,11 +879,6 @@ 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 - } cfg := ps.Config diff --git a/pkg/cmd/pulumi/destroy.go b/pkg/cmd/pulumi/destroy.go index 8ecc50ca5516..51b751e66334 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 81f6386ed2a0..9a1dbb18680a 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 f98dffd4700d..f14e89814e7c 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 d06287f84cc7..5fac12921016 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 5643a04add38..ae3c4c04f7f9 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.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.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 f1de99ee8819..b699810a35c7 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.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..dc75b75645c0 100644 --- a/sdk/go/common/workspace/config.go +++ b/sdk/go/common/workspace/config.go @@ -67,7 +67,8 @@ func missingProjectConfigurationKeysError(missingProjectKeys []string, stackName func ValidateStackConfigAndApplyProjectConfig( stackName string, project *Project, - stackConfig config.Map) error { + stackConfig config.Map, + dec config.Decrypter) error { if len(project.Config) > 0 { // only when the project defines config values, do we need to validate the stack config @@ -146,9 +147,28 @@ func ValidateStackConfigAndApplyProjectConfig( // 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 + + // 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) { 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()