Skip to content

Commit

Permalink
Stack config values do need to be declared by the project and return …
Browse files Browse the repository at this point in the history
…error when type is array without items
  • Loading branch information
Zaid-Ajaj committed Oct 30, 2022
1 parent 07e30dc commit c5c2c8f
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 97 deletions.
47 changes: 0 additions & 47 deletions sdk/go/common/workspace/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,6 @@ func missingStackConfigurationKeysError(missingKeys []string, stackName string)
formatMissingKeys(missingKeys))
}

func missingProjectConfigurationKeysError(missingProjectKeys []string, stackName string) error {
valueOrValues := "value"
if len(missingProjectKeys) > 1 {
valueOrValues = "values"
}

isOrAre := "is"
if len(missingProjectKeys) > 1 {
isOrAre = "are"
}

return fmt.Errorf(
"Stack '%v' uses configuration %v %v which %v not defined by the project configuration",
stackName,
valueOrValues,
formatMissingKeys(missingProjectKeys),
isOrAre)
}

type StackName = string
type ProjectConfigKey = string
type StackConfigValidator = func(StackName, ProjectConfigKey, ProjectConfigType, config.Value, config.Decrypter) error
Expand Down Expand Up @@ -146,34 +127,6 @@ func ValidateStackConfigAndMergeProjectConfig(
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
// for each stack config key, which is namespaced by the project, check if it is in the project config
stackConfigKeysNotDefinedByProject := []string{}
for key := range stackConfig {
if key.Namespace() != project.Name.String() {
// skip keys that are not namespaced by the project
// these could be things like aws credentials which are OK to be different across stacks
// and do not need to be defined in the project config
continue
}

namespacedKey := fmt.Sprintf("%s:%s", key.Namespace(), key.Name())
if key.Namespace() == string(project.Name) {
// then the namespace is implied and can be omitted
namespacedKey = key.Name()
}

if _, ok := project.Config[namespacedKey]; !ok {
stackConfigKeysNotDefinedByProject = append(stackConfigKeysNotDefinedByProject, namespacedKey)
}
}

if len(stackConfigKeysNotDefinedByProject) > 0 {
return missingProjectConfigurationKeysError(stackConfigKeysNotDefinedByProject, stackName)
}
}

var decrypter config.Decrypter
missingConfigurationKeys := make([]string, 0)
projectName := project.Name.String()
Expand Down
7 changes: 7 additions & 0 deletions sdk/go/common/workspace/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,12 @@ func (proj *Project) Validate() error {

if configKeyIsNamespacedByProject(projectName, configKey) {
// namespaced by project, then we have a config _type_ with a schema

if configType.IsExplicitlyTyped() && configType.TypeName() == "array" && configType.Items == nil {
return errors.Errorf("The configuration key '%v' declares an array "+
"but does not specify the underlying type via the 'items' attribute", configKey)
}

if configType.IsExplicitlyTyped() && configType.Default != nil {
if !ValidateConfigValue(configTypeName, configType.Items, configType.Default) {
inferredTypeName := InferFullTypeName(configTypeName, configType.Items)
Expand All @@ -459,6 +465,7 @@ func (proj *Project) Validate() error {
inferredTypeName)
}
}

} else {
// when not namespaced by project, there shouldn't be a type, only a value
if configType.IsExplicitlyTyped() {
Expand Down
106 changes: 56 additions & 50 deletions sdk/go/common/workspace/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,30 @@ config:
assert.Equal(t, "42", getConfigValue(t, stack.Config, "aws:answer"))
}

func TestUntypedStackConfigValuesDoNeedProjectDeclaration(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
createVpc: true`

projectStackYaml := `
config:
instanceSize: 42`

project, projectError := loadProjectFromText(t, projectYaml)
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, config.NewPanicCrypter())
assert.NoError(t, configError, "Config override should be valid")
assert.Equal(t, 2, len(stack.Config), "Stack config now has three values")
// value of instanceSize is overwritten from the stack
assert.Equal(t, "42", getConfigValue(t, stack.Config, "test:instanceSize"))
assert.Equal(t, "true", getConfigValue(t, stack.Config, "test:createVpc"))
}

func TestNamespacedProjectConfigShouldNotBeExplicitlyTyped(t *testing.T) {
t.Parallel()
projectYaml := `
Expand All @@ -526,6 +550,38 @@ config:
"Configuration key 'aws:region' is not namespaced by the project and should not define a type")
}

func TestProjectConfigCannotHaveBothValueAndDefault(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
instanceSize:
type: string
default: t3.micro
value: t4.large`

_, projectError := loadProjectFromText(t, projectYaml)
assert.Contains(t, projectError.Error(),
"project config 'instanceSize' cannot have both a 'default' and 'value' attribute")
}

func TestProjectConfigCannotBeTypedArrayWithoutItems(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
instanceSize:
type: array
default: [t3.micro, t4.large]`

_, projectError := loadProjectFromText(t, projectYaml)
assert.Contains(t, projectError.Error(),
"The configuration key 'instanceSize' declares an array "+
"but does not specify the underlying type via the 'items' attribute")
}

func TestNamespacedProjectConfigShouldNotBeProvideDefault(t *testing.T) {
t.Parallel()
projectYaml := `
Expand Down Expand Up @@ -751,56 +807,6 @@ config:
assert.Contains(t, configError.Error(), "Stack 'dev' is missing configuration values 'hello', 'values' and 'world'")
}

func TestStackConfigErrorsWhenUsingConfigValuesNotDefinedByProject(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
hello:
type: integer`

projectStackYaml := `
config:
hello: 21
world: 42`

project, projectError := loadProjectFromText(t, projectYaml)
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, 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)
}

func TestStackConfigErrorsWhenUsingMultipleConfigValuesNotDefinedByProject(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
hello:
type: integer`

projectStackYaml := `
config:
hello: 21
world: 42
another: 42`

project, projectError := loadProjectFromText(t, projectYaml)
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, 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"
assert.Contains(t, configError.Error(), expectedErrorMsg)
}

func TestStackConfigDoesNotErrorWhenProjectHasNotDefinedConfig(t *testing.T) {
t.Parallel()
projectYaml := `
Expand Down

0 comments on commit c5c2c8f

Please sign in to comment.