pulumi/sdk/go/common/workspace/project_test.go

1804 lines
50 KiB
Go

// Copyright 2018-2024, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package workspace
import (
"context"
"encoding/json"
"fmt"
"os"
"testing"
"github.com/pulumi/esc"
"github.com/pulumi/pulumi/sdk/v3/go/common/encoding"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
)
func TestProjectRuntimeInfoRoundtripYAML(t *testing.T) {
t.Parallel()
doTest := func(marshal func(interface{}) ([]byte, error), unmarshal func([]byte, interface{}) error) {
ri := NewProjectRuntimeInfo("nodejs", nil)
byts, err := marshal(ri)
assert.NoError(t, err)
var riRountrip ProjectRuntimeInfo
err = unmarshal(byts, &riRountrip)
assert.NoError(t, err)
assert.Equal(t, "nodejs", riRountrip.Name())
assert.Nil(t, riRountrip.Options())
ri = NewProjectRuntimeInfo("nodejs", map[string]interface{}{
"typescript": true,
"stringOption": "hello",
})
byts, err = marshal(ri)
assert.NoError(t, err)
err = unmarshal(byts, &riRountrip)
assert.NoError(t, err)
assert.Equal(t, "nodejs", riRountrip.Name())
assert.Equal(t, true, riRountrip.Options()["typescript"])
assert.Equal(t, "hello", riRountrip.Options()["stringOption"])
}
doTest(yaml.Marshal, yaml.Unmarshal)
doTest(json.Marshal, json.Unmarshal)
}
func TestProjectValidationForNameAndRuntime(t *testing.T) {
t.Parallel()
var err error
// Test lack of name
proj := Project{}
err = proj.Validate()
assert.EqualError(t, err, "project is missing a 'name' attribute")
// Test lack of runtime
proj.Name = "a project"
err = proj.Validate()
assert.EqualError(t, err, "project is missing a 'runtime' attribute")
// Test success
proj.Runtime = NewProjectRuntimeInfo("test", nil)
err = proj.Validate()
assert.NoError(t, err)
}
func TestProjectValidationFailsForIncorrectDefaultValueType(t *testing.T) {
t.Parallel()
project := Project{Name: "test", Runtime: NewProjectRuntimeInfo("dotnet", nil)}
invalidConfig := make(map[string]ProjectConfigType)
integerType := "integer"
invalidConfig["instanceSize"] = ProjectConfigType{
Type: &integerType,
Items: nil,
Default: "hello",
}
project.Config = invalidConfig
err := project.Validate()
assert.ErrorContains(t, err,
"The default value specified for configuration key 'instanceSize' is not of the expected type 'integer'")
invalidValues := make([]interface{}, 0)
invalidValues = append(invalidValues, "hello")
// default value here has type array<string>
// config type specified is array<array<string>>
// should fail!
arrayType := "array"
invalidConfigWithArray := make(map[string]ProjectConfigType)
invalidConfigWithArray["values"] = ProjectConfigType{
Type: &arrayType,
Items: &ProjectConfigItemsType{
Type: "array",
Items: &ProjectConfigItemsType{
Type: "string",
},
},
Default: invalidValues,
}
project.Config = invalidConfigWithArray
err = project.Validate()
assert.ErrorContains(t, err,
"The default value specified for configuration key 'values' is not of the expected type 'array<array<string>>'")
}
func TestProjectValidationSucceedsForCorrectDefaultValueType(t *testing.T) {
t.Parallel()
project := Project{Name: "test", Runtime: NewProjectRuntimeInfo("dotnet", nil)}
integerType := "integer"
validConfig := make(map[string]ProjectConfigType)
validConfig["instanceSize"] = ProjectConfigType{
Type: &integerType,
Items: nil,
Default: 1,
}
project.Config = validConfig
err := project.Validate()
assert.NoError(t, err, "There should be no validation error")
// validValues = ["hello"]
validValues := make([]interface{}, 0)
validValues = append(validValues, "hello")
// validValuesArray = [["hello"]]
validValuesArray := make([]interface{}, 0)
validValuesArray = append(validValuesArray, validValues)
// default value here has type array<array<string>>
// config type specified is also array<array<string>>
// should succeed
arrayType := "array"
validConfigWithArray := make(map[string]ProjectConfigType)
validConfigWithArray["values"] = ProjectConfigType{
Type: &arrayType,
Items: &ProjectConfigItemsType{
Type: "array",
Items: &ProjectConfigItemsType{
Type: "string",
},
},
Default: validValuesArray,
}
project.Config = validConfigWithArray
err = project.Validate()
assert.NoError(t, err, "There should be no validation error")
}
func writeAndLoad(t *testing.T, str string) (*Project, error) {
tmp, err := os.CreateTemp("", "*.json")
assert.NoError(t, err)
path := tmp.Name()
err = os.WriteFile(path, []byte(str), 0o600)
assert.NoError(t, err)
return LoadProject(path)
}
func TestProjectLoadJSON(t *testing.T) {
t.Parallel()
// Test wrong type
t.Run("wrong type", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, "\"hello \"")
// Assert.
assert.ErrorContains(t, err, "expected project to be an object, was 'string'")
})
t.Run("missing name attribute", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, "{}")
// Assert.
assert.ErrorContains(t, err, "project is missing a 'name' attribute")
})
t.Run("bad name", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, "{\"name\": \"\"}")
// Assert.
assert.ErrorContains(t, err, "project is missing a non-empty string 'name' attribute")
})
t.Run("missing runtime", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, "{\"name\": \"project\"}")
// Assert.
assert.ErrorContains(t, err, "project is missing a 'runtime' attribute")
})
t.Run("multiple errors 1", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, "{\"name\": \"project\", \"runtime\": 4}")
// Assert.
// The order can vary here, so we use Contains and not Equals.
expected := []string{
"3 errors occurred:",
"* #/runtime: oneOf failed",
"* #/runtime: expected string, but got number",
"* #/runtime: expected object, but got number",
}
for _, e := range expected {
assert.ErrorContains(t, err, e)
}
})
t.Run("multiple errors, 2", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, "{\"name\": \"project\", \"runtime\": \"test\", \"backend\": 4, \"main\": {}}")
// Assert.
// The order can vary here, so we use Contains and not Equals.
expected := []string{
"2 errors occurred:",
"* #/main: expected string or null, but got object",
"* #/backend: expected object or null, but got number",
}
for _, e := range expected {
assert.ErrorContains(t, err, e)
}
})
t.Run("success", func(t *testing.T) {
t.Parallel()
// Act.
proj, err := writeAndLoad(t, "{\"name\": \"project\", \"runtime\": \"test\"}")
// Assert.
assert.NoError(t, err)
assert.Equal(t, tokens.PackageName("project"), proj.Name)
assert.Equal(t, "test", proj.Runtime.Name())
})
t.Run("null optionals should work", func(t *testing.T) {
t.Parallel()
// Act.
proj, err := writeAndLoad(t, "{\"name\": \"project\", \"runtime\": \"test\", "+
"\"description\": null, \"main\": null, \"backend\": null}")
// Assert.
assert.NoError(t, err)
assert.Nil(t, proj.Description)
assert.Equal(t, "", proj.Main)
})
}
func TestProjectLoadJSONInformativeErrors(t *testing.T) {
t.Parallel()
t.Run("a missing name attribute", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, `{"Name": "project", "runtime": "test"}`)
// Assert.
assert.ErrorContains(t, err, "project is missing a 'name' attribute")
assert.ErrorContains(t, err, "found 'Name' instead")
})
t.Run("a missing runtime attribute", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, `{"name": "project", "rutnime": "test"}`)
// Assert.
assert.ErrorContains(t, err, "project is missing a 'runtime' attribute")
assert.ErrorContains(t, err, "found 'rutnime' instead")
})
t.Run("a minor spelling mistake in a schema field", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, `{
"name": "project",
"runtime": "test",
"template": {
"displatName": "foo"
}
}`)
// Assert.
assert.ErrorContains(t, err, "'displatName' not allowed; did you mean 'displayName'?")
})
t.Run("a major spelling mistake in a schema field", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, `{
"name": "project",
"runtime": "test",
"template": {
"displayNameDisplayName": "foo"
}
}`)
// Assert.
assert.ErrorContains(t, err, "'displayNameDisplayName' not allowed")
assert.ErrorContains(t, err, "'displayNameDisplayName' not allowed; the allowed attributes are "+
"'config', 'description', 'displayName', 'important', 'metadata' and 'quickstart'")
})
t.Run("specific errors when only a single attribute is expected", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, `{
"name": "project",
"runtime": "test",
"backend": {
"url": "https://pulumi.com",
"name": "test"
}
}`)
// Assert.
assert.ErrorContains(t, err, "'name' not allowed")
assert.ErrorContains(t, err, "'name' not allowed; the only allowed attribute is 'url'")
})
t.Run("a minor spelling mistake even deeper in the schema", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, `{
"name": "project",
"runtime": "test",
"plugins": {
"providers": [
{
"nome": "test"
}
]
}
}`)
// Assert.
assert.ErrorContains(t, err, "'nome' not allowed; did you mean 'name'")
})
t.Run("a major spelling mistake even deeper in the schema", func(t *testing.T) {
t.Parallel()
// Act.
_, err := writeAndLoad(t, `{
"name": "project",
"runtime": "test",
"plugins": {
"providers": [
{
"displayName": "test"
}
]
}
}`)
// Assert.
assert.ErrorContains(t, err, "'displayName' not allowed")
assert.ErrorContains(t, err, "'displayName' not allowed; the allowed attributes are "+
"'name', 'path' and 'version'")
})
}
func deleteFile(t *testing.T, file *os.File) {
if file != nil {
err := os.Remove(file.Name())
assert.NoError(t, err, "Error while deleting file")
}
}
func loadProjectFromText(t *testing.T, content string) (*Project, error) {
tmp, err := os.CreateTemp("", "*.yaml")
assert.NoError(t, err)
path := tmp.Name()
err = os.WriteFile(path, []byte(content), 0o600)
assert.NoError(t, err)
defer deleteFile(t, tmp)
return LoadProject(path)
}
func loadProjectStackFromText(t *testing.T, project *Project, content string) (*ProjectStack, error) {
tmp, err := os.CreateTemp("", "*.yaml")
assert.NoError(t, err)
path := tmp.Name()
err = os.WriteFile(path, []byte(content), 0o600)
assert.NoError(t, err)
defer deleteFile(t, tmp)
return LoadProjectStack(project, path)
}
func loadProjectStackFromJSONText(t *testing.T, project *Project, content string) (*ProjectStack, error) {
tmp, err := os.CreateTemp("", "*.json")
assert.NoError(t, err)
path := tmp.Name()
err = os.WriteFile(path, []byte(content), 0o600)
assert.NoError(t, err)
defer deleteFile(t, tmp)
return LoadProjectStack(project, path)
}
func TestProjectLoadsConfigSchemas(t *testing.T) {
t.Parallel()
projectContent := `
name: test
runtime: dotnet
config:
integerSchemaFull:
type: integer
description: a very important value
default: 1
integerSchemaSimple: 20
textSchemaFull:
type: string
default: t3.micro
textSchemaSimple: t4.large
booleanSchemaFull:
type: boolean
default: true
booleanSchemaSimple: false
simpleArrayOfStrings:
type: array
items:
type: string
default: [hello]
arrayOfArrays:
type: array
items:
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, 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.TypeName())
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, "", integerSchemaSimple.TypeName(), "not explicitly typed")
assert.False(t, integerSchemaSimple.IsExplicitlyTyped())
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.TypeName())
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, "", textSchemaSimple.TypeName(), "not explicitly typed")
assert.False(t, textSchemaSimple.IsExplicitlyTyped())
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.TypeName())
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, "", booleanSchemaSimple.TypeName(), "not explicitly typed")
assert.False(t, booleanSchemaSimple.IsExplicitlyTyped())
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.TypeName())
assert.False(t, simpleArrayOfStrings.Secret)
assert.NotNil(t, simpleArrayOfStrings.Items)
assert.Equal(t, "string", simpleArrayOfStrings.Items.Type)
arrayValues := simpleArrayOfStrings.Default.([]interface{})
assert.Equal(t, "hello", arrayValues[0])
arrayOfArrays, ok := project.Config["arrayOfArrays"]
assert.True(t, ok, "should be able to read arrayOfArrays")
assert.Equal(t, "array", arrayOfArrays.TypeName())
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.TypeName())
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 {
parsedKey, err := config.ParseKey(key)
assert.NoErrorf(t, err, "There should be no error parsing the config key '%v'", key)
configValue, foundValue := stackConfig[parsedKey]
assert.Truef(t, foundValue, "Couldn't find a value for config key %v", key)
value, valueError := configValue.Value(config.NopDecrypter)
assert.NoErrorf(t, valueError, "Error while getting the value for key %v", key)
return value
}
func getConfigValueUnmarshalled(t *testing.T, stackConfig config.Map, key string) interface{} {
parsedKey, err := config.ParseKey(key)
assert.NoErrorf(t, err, "There should be no error parsing the config key '%v'", key)
configValue, foundValue := stackConfig[parsedKey]
assert.Truef(t, foundValue, "Couldn't find a value for config key %v", key)
valueJSON, valueError := configValue.Value(config.NopDecrypter)
assert.NoErrorf(t, valueError, "Error while getting the value for key %v", key)
var value interface{}
err = json.Unmarshal([]byte(valueJSON), &value)
assert.NoErrorf(t, err, "Error while unmarshalling value for key %v", key)
return value
}
func TestStackConfigIsInheritedFromProjectConfig(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
instanceSize: t3.micro
instanceCount: 20
protect: true`
projectStackYaml := `
config:
test:instanceSize: t4.large`
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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
config.NewPanicCrypter())
assert.NoError(t, configError, "Config override should be valid")
assert.Equal(t, 3, len(stack.Config), "Stack config now has three values")
// value of instanceSize is overwritten from the stack
assert.Equal(t, "t4.large", getConfigValue(t, stack.Config, "test:instanceSize"))
// instanceCount and protect are inherited from the project
assert.Equal(t, "20", getConfigValue(t, stack.Config, "test:instanceCount"))
assert.Equal(t, "true", getConfigValue(t, stack.Config, "test:protect"))
}
func TestNamespacedConfigValuesAreInheritedCorrectly(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
aws:region: us-west-1
pulumi:disable-default-providers: ["*"]
instanceSize: t3.micro`
projectStackYaml := `
config:
test:instanceSize: t4.large`
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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
config.NewPanicCrypter())
assert.NoError(t, configError, "Config override should be valid")
assert.Equal(t, 3, len(stack.Config), "Stack config now has three values")
// value of instanceSize is overwritten from the stack
assert.Equal(t, "t4.large", getConfigValue(t, stack.Config, "test:instanceSize"))
// aws:region is namespaced and is inherited from the project
assert.Equal(t, "us-west-1", getConfigValue(t, stack.Config, "aws:region"))
assert.Equal(t, "[\"*\"]", getConfigValue(t, stack.Config, "pulumi:disable-default-providers"))
assert.Equal(t, []interface{}{"*"}, getConfigValueUnmarshalled(t, stack.Config, "pulumi:disable-default-providers"))
}
func TestLoadingStackConfigWithoutNamespacingTheProject(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
aws:region: us-west-1
instanceSize: t3.micro`
projectStackYaml := `
config:
instanceSize: t4.large`
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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
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, "t4.large", getConfigValue(t, stack.Config, "test:instanceSize"))
// aws:region is namespaced and is inherited from the project
assert.Equal(t, "us-west-1", getConfigValue(t, stack.Config, "aws:region"))
}
func TestUntypedProjectConfigValuesAreNotValidated(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
instanceSize: t3.micro
aws:region: us-west-1`
projectStackYaml := `
config:
instanceSize: 9999
aws:region: 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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
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, "9999", getConfigValue(t, stack.Config, "test:instanceSize"))
assert.Equal(t, "42", getConfigValue(t, stack.Config, "aws:region"))
}
func TestUntypedProjectConfigValuesWithOnlyDefaultOrOnlyValue(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
instanceSize:
default: t3.micro
region:
value: us-west-1`
projectStackYaml := `
config:
aws:answer: 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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
config.NewPanicCrypter())
assert.NoError(t, configError, "Config override should be valid")
assert.Equal(t, 3, len(stack.Config), "Stack config now has three values")
// value of instanceSize is overwritten from the stack
assert.Equal(t, "t3.micro", getConfigValue(t, stack.Config, "test:instanceSize"))
assert.Equal(t, "us-west-1", getConfigValue(t, stack.Config, "test:region"))
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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
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 := `
name: test
runtime: dotnet
config:
aws:region:
type: string
value:
region: us-west-1`
_, projectError := loadProjectFromText(t, projectYaml)
assert.ErrorContains(t, projectError,
"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.ErrorContains(t, projectError,
"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.ErrorContains(t, projectError,
"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 := `
name: test
runtime: dotnet
config:
aws:region:
default: us-west-1`
_, projectError := loadProjectFromText(t, projectYaml)
assert.ErrorContains(t, projectError,
"Configuration key 'aws:region' is not namespaced by the project and should not define a default value")
assert.ErrorContains(t, projectError,
"Did you mean to use the 'value' attribute instead of 'default'?")
}
func TestUntypedProjectConfigObjectValuesPassedDownToStack(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
instanceSize:
value:
hello: world
aws:config:
value:
region: us-west-1`
projectStackYaml := `
config:
aws:whatever: 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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
config.NewPanicCrypter())
assert.NoError(t, configError, "Config override should be valid")
assert.Equal(t, 3, len(stack.Config), "Stack config now has three values")
// value of instanceSize is overwritten from the stack
assert.Equal(t, "{\"hello\":\"world\"}", getConfigValue(t, stack.Config, "test:instanceSize"))
assert.Equal(t, "{\"region\":\"us-west-1\"}", getConfigValue(t, stack.Config, "aws:config"))
assert.Equal(t, "42", getConfigValue(t, stack.Config, "aws:whatever"))
}
func TestStackConfigErrorsWhenStackValueIsNotCorrectlyTyped(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
values:
type: array
items:
type: string
default: [value]`
projectStackYaml := `
config:
test:values: someValue
`
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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
config.NewPanicCrypter())
assert.ErrorContains(t, configError, "Stack 'dev' with configuration key 'values' must be of type 'array<string>'")
}
func TestLoadingConfigIsRewrittenToStackConfigDir(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config: ./some/path`
project, projectError := loadProjectFromText(t, projectYaml)
assert.NoError(t, projectError, "Shold be able to load the project")
assert.Equal(t, "./some/path", project.StackConfigDir, "Stack config dir is read from the config property")
assert.Equal(t, 0, len(project.Config), "Config should be empty")
}
func TestDefningBothConfigAndStackConfigDirErrorsOut(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config: ./some/path
stackConfigDir: ./some/other/path`
project, projectError := loadProjectFromText(t, projectYaml)
assert.Nil(t, project, "Should NOT be able to load the project")
assert.ErrorContains(t, projectError, "Should not use both config and stackConfigDir")
}
func TestConfigObjectAndStackConfigDirSuccessfullyLoadProject(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
stackConfigDir: ./some/other/path
config:
value: hello
`
project, projectError := loadProjectFromText(t, projectYaml)
assert.Nil(t, projectError, "There is no error")
assert.NotNil(t, project, "The project can be loaded correctly")
assert.Equal(t, "./some/other/path", project.StackConfigDir)
assert.Equal(t, 1, len(project.Config), "there is one config value")
}
func TestStackConfigIntegerTypeIsCorrectlyValidated(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
importantNumber:
type: integer
`
projectStackYamlValid := `
config:
test:importantNumber: 20
`
projectStackYamlInvalid := `
config:
test:importantNumber: hello
`
ctx := context.Background()
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(
ctx,
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
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(
ctx,
"dev",
project,
esc.Value{},
invalidStackConfig.Config,
config.NewPanicCrypter(),
config.NewPanicCrypter())
assert.ErrorContains(t, configError,
"Stack 'dev' with configuration key 'importantNumber' must be of type 'integer'")
}
func TestStackConfigErrorsWhenMissingStackValueForConfigTypeWithNoDefault(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
values:
type: array
items:
type: string`
projectStackYaml := ``
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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
config.NewPanicCrypter())
assert.ErrorContains(t, configError, "Stack 'dev' is missing configuration value 'values'")
}
func TestStackConfigErrorsWhenMissingTwoStackValueForConfigTypeWithNoDefault(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
another:
type: string
values:
type: array
items:
type: string`
projectStackYaml := ``
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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
config.NewPanicCrypter())
assert.ErrorContains(t, configError, "Stack 'dev' is missing configuration values 'another' and 'values'")
}
func TestStackConfigErrorsWhenMissingMultipleStackValueForConfigTypeWithNoDefault(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet
config:
hello:
type: integer
values:
type: array
items:
type: string
world:
type: string`
projectStackYaml := ``
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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
config.NewPanicCrypter())
assert.ErrorContains(t, configError, "Stack 'dev' is missing configuration values 'hello', 'values' and 'world'")
}
func TestStackConfigDoesNotErrorWhenProjectHasNotDefinedConfig(t *testing.T) {
t.Parallel()
projectYaml := `
name: test
runtime: dotnet`
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(
context.Background(),
"dev",
project,
esc.Value{},
stack.Config,
config.NewPanicCrypter(),
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
`
ctx := context.Background()
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(
ctx,
"dev",
project,
esc.Value{},
stack.Config,
crypter,
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(
ctx,
"dev",
project,
esc.Value{},
invalidStackConfig.Config,
crypter,
crypter)
assert.ErrorContains(t, configError,
"Stack 'dev' with configuration key 'importantNumber' must be encrypted as it's secret")
}
//nolint:lll
func TestEnvironmentMerge(t *testing.T) {
t.Parallel()
projectYAML := `
name: test
runtime: nodejs`
stackYAML := `
config:
test:boolean: true
test:number: 42
test:string: foo
test:array:
- first
- second
test:object:
foo: bar
object:
baz: 42`
env := esc.NewValue(map[string]esc.Value{
"test:boolean": esc.NewValue(false),
"test:number": esc.NewValue(json.Number("42")),
"test:string": esc.NewValue("esc"),
"test:array": esc.NewValue([]esc.Value{esc.NewValue("second"), esc.NewValue("first")}),
"test:secret": esc.NewSecret("hunter2"),
"test:object": esc.NewValue(map[string]esc.Value{
"boolean": esc.NewValue(true),
"number": esc.NewValue(json.Number("42")),
"string": esc.NewValue("esc"),
"array": esc.NewValue([]esc.Value{esc.NewValue("first"), esc.NewValue("second")}),
"object": esc.NewValue(map[string]esc.Value{"foo": esc.NewValue("bar")}),
"foo": esc.NewValue("qux"),
}),
})
project, projectError := loadProjectFromText(t, projectYAML)
require.NoError(t, projectError, "Shold be able to load the project")
stack, stackError := loadProjectStackFromText(t, project, stackYAML)
require.NoError(t, stackError, "Should be able to read the stack")
configError := ValidateStackConfigAndApplyProjectConfig(
context.Background(),
"dev",
project,
env,
stack.Config,
config.Base64Crypter,
config.Base64Crypter)
require.NoError(t, configError, "there should not be a config type error")
secureKeys := stack.Config.SecureKeys()
assert.Equal(t, []config.Key{config.MustMakeKey("test", "secret")}, secureKeys)
m, err := stack.Config.Decrypt(config.Base64Crypter)
require.NoError(t, err)
expected := map[config.Key]string{
config.MustMakeKey("test", "array"): "[\"first\",\"second\"]",
config.MustMakeKey("test", "boolean"): "true",
config.MustMakeKey("test", "number"): "42",
config.MustMakeKey("test", "object"): "{\"array\":[\"first\",\"second\"],\"boolean\":true,\"foo\":\"bar\",\"number\":42,\"object\":{\"baz\":42,\"foo\":\"bar\"},\"string\":\"esc\"}",
config.MustMakeKey("test", "string"): "foo",
config.MustMakeKey("test", "secret"): "hunter2",
}
assert.Equal(t, expected, m)
}
func TestProjectLoadYAML(t *testing.T) {
t.Parallel()
// Test wrong type
_, err := loadProjectFromText(t, "\"hello\"")
assert.ErrorContains(t, err, "expected project to be an object")
// Test bad key
_, err = loadProjectFromText(t, "4: hello")
assert.ErrorContains(t, err, "expected only string keys, got '%!s(int=4)'")
// Test nested bad key
_, err = loadProjectFromText(t, "hello:\n 6: bad")
assert.ErrorContains(t, err, "project is missing a 'name' attribute")
// Test lack of name
_, err = loadProjectFromText(t, "{}")
assert.ErrorContains(t, err, "project is missing a 'name' attribute")
// Test bad name
_, err = loadProjectFromText(t, "name:")
assert.ErrorContains(t, err, "project is missing a non-empty string 'name' attribute")
// Test missing runtime
_, err = loadProjectFromText(t, "name: project")
assert.ErrorContains(t, err, "project is missing a 'runtime' attribute")
// Test other schema errors
_, err = loadProjectFromText(t, "name: project\nruntime: 4")
// These can vary in order, so contains not equals check
expected := []string{
"3 errors occurred:",
"* #/runtime: oneOf failed",
"* #/runtime: expected string, but got number",
"* #/runtime: expected object, but got number",
}
for _, e := range expected {
assert.ErrorContains(t, err, e)
}
_, err = loadProjectFromText(t, "name: project\nruntime: test\nbackend: 4\nmain: {}")
expected = []string{
"2 errors occurred:",
"* #/main: expected string or null, but got object",
"* #/backend: expected object or null, but got number",
}
for _, e := range expected {
assert.ErrorContains(t, err, e)
}
// Test success
proj, err := loadProjectFromText(t, "name: project\nruntime: test")
assert.NoError(t, err)
assert.Equal(t, tokens.PackageName("project"), proj.Name)
assert.Equal(t, "test", proj.Runtime.Name())
// Test null optionals should work
proj, err = loadProjectFromText(t, "name: project\nruntime: test\ndescription:\nmain: null\nbackend:\n")
assert.NoError(t, err)
assert.Nil(t, proj.Description)
assert.Equal(t, "", proj.Main)
}
func TestProjectSaveLoadRoundtrip(t *testing.T) {
t.Parallel()
tests := []struct {
name string
project Project
}{
{
name: "Numeric name",
project: Project{
Name: "1234",
Runtime: NewProjectRuntimeInfo("python", nil),
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tmp, err := os.CreateTemp("", "*.yaml")
require.NoError(t, err)
defer deleteFile(t, tmp)
path := tmp.Name()
err = tt.project.Save(path)
require.NoError(t, err)
loadedProject, err := LoadProject(path)
require.NoError(t, err)
require.NotNil(t, loadedProject)
// Clear the raw data before we compare
loadedProject.raw = nil
assert.Equal(t, tt.project, *loadedProject)
})
}
}
func TestProjectEditRoundtrip(t *testing.T) {
t.Parallel()
tests := []struct {
name string
yaml string
edit func(*Project)
expected string
}{
{
name: "Change name",
yaml: "name: test\nruntime: python\n",
edit: func(proj *Project) { proj.Name = "new" },
expected: "name: new\nruntime: python\n",
},
{
name: "Add runtime option",
yaml: "name: test\nruntime: python\n",
edit: func(proj *Project) {
proj.Runtime = NewProjectRuntimeInfo(
proj.Runtime.Name(),
map[string]interface{}{
"setting": "test",
})
},
expected: "name: test\nruntime:\n name: python\n options:\n setting: test\n",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
tmp, err := os.CreateTemp("", "*.yaml")
require.NoError(t, err)
defer deleteFile(t, tmp)
path := tmp.Name()
err = os.WriteFile(path, []byte(tt.yaml), 0o600)
require.NoError(t, err)
loadedProject, err := LoadProject(path)
require.NoError(t, err)
require.NotNil(t, loadedProject)
tt.edit(loadedProject)
err = loadedProject.Save(path)
require.NoError(t, err)
actualYaml, err := os.ReadFile(path)
require.NoError(t, err)
assert.Equal(t, tt.expected, string(actualYaml))
})
}
}
func TestEnvironmentAppend(t *testing.T) {
t.Parallel()
t.Run("JSON list", func(t *testing.T) {
t.Parallel()
projectYaml := `name: test
runtime: yaml`
projectStackJSON := "{}"
project, err := loadProjectFromText(t, projectYaml)
require.NoError(t, err)
stack, err := loadProjectStackFromJSONText(t, project, projectStackJSON)
require.NoError(t, err)
stack.Environment = stack.Environment.Append("env")
marshaled, err := encoding.JSON.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, "{\n \"environment\": [\n \"env\"\n ]\n}\n", string(marshaled))
stack.Environment = stack.Environment.Append("env2")
marshaled, err = encoding.JSON.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, "{\n \"environment\": [\n \"env\",\n \"env2\"\n ]\n}\n", string(marshaled))
})
t.Run("JSON literal", func(t *testing.T) {
t.Parallel()
projectYaml := `name: test
runtime: yaml`
projectStackJSON := `{
"environment": {
"values": {
"pulumiConfig": {
"aws:region": "us-west-2"
}
}
}
}
`
project, err := loadProjectFromText(t, projectYaml)
require.NoError(t, err)
stack, err := loadProjectStackFromJSONText(t, project, projectStackJSON)
require.NoError(t, err)
expected := `{
"environment": {
"imports": [
"env"
],
"values": {
"pulumiConfig": {
"aws:region": "us-west-2"
}
}
}
}
`
stack.Environment = stack.Environment.Append("env")
marshaled, err := encoding.JSON.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
expected = `{
"environment": {
"imports": [
"env",
"env2"
],
"values": {
"pulumiConfig": {
"aws:region": "us-west-2"
}
}
}
}
`
stack.Environment = stack.Environment.Append("env2")
marshaled, err = encoding.JSON.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
})
t.Run("YAML list", func(t *testing.T) {
t.Parallel()
projectYaml := `name: test
runtime: yaml`
projectStackYaml := ""
project, err := loadProjectFromText(t, projectYaml)
require.NoError(t, err)
stack, err := loadProjectStackFromText(t, project, projectStackYaml)
require.NoError(t, err)
stack.Environment = stack.Environment.Append("env")
marshaled, err := encoding.YAML.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, "environment:\n - env\n", string(marshaled))
stack.Environment = stack.Environment.Append("env2")
marshaled, err = encoding.YAML.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, "environment:\n - env\n - env2\n", string(marshaled))
})
t.Run("YAML literal", func(t *testing.T) {
t.Parallel()
projectYaml := `name: test
runtime: yaml`
projectStackYaml := `environment:
values:
pulumiConfig:
aws:region: us-west-2`
project, err := loadProjectFromText(t, projectYaml)
require.NoError(t, err)
stack, err := loadProjectStackFromText(t, project, projectStackYaml)
require.NoError(t, err)
expected := `environment:
values:
pulumiConfig:
aws:region: us-west-2
imports:
- env
`
stack.Environment = stack.Environment.Append("env")
marshaled, err := encoding.YAML.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
expected = `environment:
values:
pulumiConfig:
aws:region: us-west-2
imports:
- env
- env2
`
stack.Environment = stack.Environment.Append("env2")
marshaled, err = encoding.YAML.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
})
}
func TestEnvironmentRemove(t *testing.T) {
t.Parallel()
t.Run("JSON list", func(t *testing.T) {
t.Parallel()
projectYaml := `name: test
runtime: yaml`
projectStackJSON := `{
"environment": [
"env2",
"env",
"env2"
]
}
`
project, err := loadProjectFromText(t, projectYaml)
require.NoError(t, err)
stack, err := loadProjectStackFromJSONText(t, project, projectStackJSON)
require.NoError(t, err)
expected := `{
"environment": [
"env2",
"env"
]
}
`
stack.Environment = stack.Environment.Remove("env2")
marshaled, err := encoding.JSON.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
expected = `{
"environment": [
"env2"
]
}
`
stack.Environment = stack.Environment.Remove("env")
marshaled, err = encoding.JSON.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
stack.Environment = stack.Environment.Remove("env2")
marshaled, err = encoding.JSON.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, "{}\n", string(marshaled))
})
t.Run("JSON literal", func(t *testing.T) {
t.Parallel()
projectYaml := `name: test
runtime: yaml`
projectStackJSON := `{
"environment": {
"imports": [
{
"env2": {
"merge": false
}
},
"env",
"env2"
],
"values": {
"pulumiConfig": {
"aws:region": "us-west-2"
}
}
}
}
`
project, err := loadProjectFromText(t, projectYaml)
require.NoError(t, err)
stack, err := loadProjectStackFromJSONText(t, project, projectStackJSON)
require.NoError(t, err)
expected := `{
"environment": {
"imports": [
{
"env2": {
"merge": false
}
},
"env"
],
"values": {
"pulumiConfig": {
"aws:region": "us-west-2"
}
}
}
}
`
stack.Environment = stack.Environment.Remove("env2")
marshaled, err := encoding.JSON.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
expected = `{
"environment": {
"imports": [
"env"
],
"values": {
"pulumiConfig": {
"aws:region": "us-west-2"
}
}
}
}
`
stack.Environment = stack.Environment.Remove("env2")
marshaled, err = encoding.JSON.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
expected = `{
"environment": {
"imports": [],
"values": {
"pulumiConfig": {
"aws:region": "us-west-2"
}
}
}
}
`
stack.Environment = stack.Environment.Remove("env")
marshaled, err = encoding.JSON.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
})
t.Run("YAML list", func(t *testing.T) {
t.Parallel()
projectYaml := `name: test
runtime: yaml`
projectStackYaml := `environment:
- env2
- env
- env2
`
project, err := loadProjectFromText(t, projectYaml)
require.NoError(t, err)
stack, err := loadProjectStackFromText(t, project, projectStackYaml)
require.NoError(t, err)
expected := `environment:
- env2
- env
`
stack.Environment = stack.Environment.Remove("env2")
marshaled, err := encoding.YAML.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
stack.Environment = stack.Environment.Remove("env")
marshaled, err = encoding.YAML.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, "environment:\n - env2\n", string(marshaled))
stack.Environment = stack.Environment.Remove("env2")
marshaled, err = encoding.YAML.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, "{}\n", string(marshaled))
})
t.Run("YAML literal", func(t *testing.T) {
t.Parallel()
projectYaml := `name: test
runtime: yaml`
projectStackYaml := `environment:
imports:
- {env2: {merge: false}}
- env
- env2
values:
pulumiConfig:
aws:region: us-west-2`
project, err := loadProjectFromText(t, projectYaml)
require.NoError(t, err)
stack, err := loadProjectStackFromText(t, project, projectStackYaml)
require.NoError(t, err)
expected := `environment:
imports:
- {env2: {merge: false}}
- env
values:
pulumiConfig:
aws:region: us-west-2
`
stack.Environment = stack.Environment.Remove("env2")
marshaled, err := encoding.YAML.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
expected = `environment:
imports:
- env
values:
pulumiConfig:
aws:region: us-west-2
`
stack.Environment = stack.Environment.Remove("env2")
marshaled, err = encoding.YAML.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
expected = `environment:
values:
pulumiConfig:
aws:region: us-west-2
`
stack.Environment = stack.Environment.Remove("env")
marshaled, err = encoding.YAML.Marshal(stack)
require.NoError(t, err)
assert.Equal(t, expected, string(marshaled))
})
}