From 79cadf2c8aa87d37a1d04f413913b5093e77b2e3 Mon Sep 17 00:00:00 2001 From: Tiago de Freitas Lima Date: Thu, 25 Apr 2024 11:09:41 -0300 Subject: [PATCH] Refactoring StructuredConfig + controller's support to ConfigRefs tests --- deploy/crds/pulumi.com_stacks.yaml | 4 +- docs/stacks.md | 80 ++--- pkg/apis/pulumi/shared/stack_types.go | 62 +++- pkg/controller/stack/stack_config.go | 54 ++- pkg/controller/stack/stack_config_test.go | 31 +- pkg/controller/stack/stack_controller.go | 22 +- test/stack_controller_test.go | 395 ++++++++++++++++++++++ 7 files changed, 544 insertions(+), 104 deletions(-) diff --git a/deploy/crds/pulumi.com_stacks.yaml b/deploy/crds/pulumi.com_stacks.yaml index 8bf9446a..13d116e9 100644 --- a/deploy/crds/pulumi.com_stacks.yaml +++ b/deploy/crds/pulumi.com_stacks.yaml @@ -80,7 +80,7 @@ spec: which can be optionally specified inline. If this is omitted, configuration is assumed to be checked in and taken from the source repository. type: object - configsRef: + configRefs: additionalProperties: description: ConfigRef identifies a resource from which config information can be loaded. Environment variables, files on the filesystem, @@ -997,7 +997,7 @@ spec: which can be optionally specified inline. If this is omitted, configuration is assumed to be checked in and taken from the source repository. type: object - configsRef: + configRefs: additionalProperties: description: ConfigRef identifies a resource from which config information can be loaded. Environment variables, files on the filesystem, diff --git a/docs/stacks.md b/docs/stacks.md index d4b818eb..e52aaba5 100644 --- a/docs/stacks.md +++ b/docs/stacks.md @@ -127,7 +127,7 @@ StackSpec defines the desired state of Pulumi Stack being managed by this operat false - configsRef + configRefs map[string]object (optional) ConfigRefs is the configuration for this stack, which can be specified through ConfigRef. is omitted, configuration is assumed to be checked in and taken from the source repository.
@@ -286,7 +286,7 @@ StackSpec defines the desired state of Pulumi Stack being managed by this operat -### Stack.spec.configsRef[key] +### Stack.spec.configRefs[key] [↩ Parent](#stackspec) @@ -310,42 +310,42 @@ ConfigRef identifies a resource from which config information can be loaded. Env true - configmap + configmap object ConfigMapRef refers to a Kubernetes ConfigMap
false - env + env object Env selects an environment variable set on the operator process
false - filesystem + filesystem object FileSystem selects a file on the operator's file system
false - literal + literal object LiteralRef refers to a literal value
false - secret + secret object SecretRef refers to a Kubernetes Secret
false - structured + structured object StructuredRef refers to a structured value
@@ -355,8 +355,8 @@ ConfigRef identifies a resource from which config information can be loaded. Env -### Stack.spec.configsRef[key].configmap -[↩ Parent](#stackspecconfigsrefkey) +### Stack.spec.configRefs[key].configmap +[↩ Parent](#stackspecconfigrefskey) @@ -396,8 +396,8 @@ ConfigMapRef refers to a Kubernetes ConfigMap -### Stack.spec.configsRef[key].env -[↩ Parent](#stackspecconfigsrefkey) +### Stack.spec.configRefs[key].env +[↩ Parent](#stackspecconfigrefskey) @@ -423,8 +423,8 @@ Env selects an environment variable set on the operator process -### Stack.spec.configsRef[key].filesystem -[↩ Parent](#stackspecconfigsrefkey) +### Stack.spec.configRefs[key].filesystem +[↩ Parent](#stackspecconfigrefskey) @@ -450,8 +450,8 @@ FileSystem selects a file on the operator's file system -### Stack.spec.configsRef[key].literal -[↩ Parent](#stackspecconfigsrefkey) +### Stack.spec.configRefs[key].literal +[↩ Parent](#stackspecconfigrefskey) @@ -477,8 +477,8 @@ LiteralRef refers to a literal value -### Stack.spec.configsRef[key].secret -[↩ Parent](#stackspecconfigsrefkey) +### Stack.spec.configRefs[key].secret +[↩ Parent](#stackspecconfigrefskey) @@ -518,8 +518,8 @@ SecretRef refers to a Kubernetes Secret -### Stack.spec.configsRef[key].structured -[↩ Parent](#stackspecconfigsrefkey) +### Stack.spec.configRefs[key].structured +[↩ Parent](#stackspecconfigrefskey) @@ -2361,7 +2361,7 @@ StackSpec defines the desired state of Pulumi Stack being managed by this operat false - configsRef + configRefs map[string]object (optional) ConfigRefs is the configuration for this stack, which can be specified through ConfigRef. is omitted, configuration is assumed to be checked in and taken from the source repository.
@@ -2520,7 +2520,7 @@ StackSpec defines the desired state of Pulumi Stack being managed by this operat -### Stack.spec.configsRef[key] +### Stack.spec.configRefs[key] [↩ Parent](#stackspec-1) @@ -2544,42 +2544,42 @@ ConfigRef identifies a resource from which config information can be loaded. Env true - configmap + configmap object ConfigMapRef refers to a Kubernetes ConfigMap
false - env + env object Env selects an environment variable set on the operator process
false - filesystem + filesystem object FileSystem selects a file on the operator's file system
false - literal + literal object LiteralRef refers to a literal value
false - secret + secret object SecretRef refers to a Kubernetes Secret
false - structured + structured object StructuredRef refers to a structured value
@@ -2589,8 +2589,8 @@ ConfigRef identifies a resource from which config information can be loaded. Env -### Stack.spec.configsRef[key].configmap -[↩ Parent](#stackspecconfigsrefkey-1) +### Stack.spec.configRefs[key].configmap +[↩ Parent](#stackspecconfigrefskey-1) @@ -2630,8 +2630,8 @@ ConfigMapRef refers to a Kubernetes ConfigMap -### Stack.spec.configsRef[key].env -[↩ Parent](#stackspecconfigsrefkey-1) +### Stack.spec.configRefs[key].env +[↩ Parent](#stackspecconfigrefskey-1) @@ -2657,8 +2657,8 @@ Env selects an environment variable set on the operator process -### Stack.spec.configsRef[key].filesystem -[↩ Parent](#stackspecconfigsrefkey-1) +### Stack.spec.configRefs[key].filesystem +[↩ Parent](#stackspecconfigrefskey-1) @@ -2684,8 +2684,8 @@ FileSystem selects a file on the operator's file system -### Stack.spec.configsRef[key].literal -[↩ Parent](#stackspecconfigsrefkey-1) +### Stack.spec.configRefs[key].literal +[↩ Parent](#stackspecconfigrefskey-1) @@ -2711,8 +2711,8 @@ LiteralRef refers to a literal value -### Stack.spec.configsRef[key].secret -[↩ Parent](#stackspecconfigsrefkey-1) +### Stack.spec.configRefs[key].secret +[↩ Parent](#stackspecconfigrefskey-1) @@ -2752,8 +2752,8 @@ SecretRef refers to a Kubernetes Secret -### Stack.spec.configsRef[key].structured -[↩ Parent](#stackspecconfigsrefkey-1) +### Stack.spec.configRefs[key].structured +[↩ Parent](#stackspecconfigrefskey-1) diff --git a/pkg/apis/pulumi/shared/stack_types.go b/pkg/apis/pulumi/shared/stack_types.go index da4d0999..0462aee5 100644 --- a/pkg/apis/pulumi/shared/stack_types.go +++ b/pkg/apis/pulumi/shared/stack_types.go @@ -48,7 +48,7 @@ type StackSpec struct { Config map[string]string `json:"config,omitempty"` // (optional) ConfigRefs is the configuration for this stack, which can be specified through ConfigRef. // is omitted, configuration is assumed to be checked in and taken from the source repository. - ConfigRefs map[string]ConfigRef `json:"configsRef,omitempty"` + ConfigRefs map[string]ConfigRef `json:"configRefs,omitempty"` // (optional) Secrets is the secret configuration for this stack, which can be optionally specified inline. If this // is omitted, secrets configuration is assumed to be checked in and taken from the source repository. // Deprecated: use SecretRefs instead. @@ -275,6 +275,16 @@ func NewEnvResourceRef(envVarName string) ResourceRef { } } +func NewEnvConfigResourceRef(envVarName string) ConfigRef { + envResourceRef := NewEnvResourceRef(envVarName) + return ConfigRef{ + SelectorType: ConfigResourceSelectorType(envResourceRef.SelectorType), + ConfigResourceSelector: ConfigResourceSelector{ + ResourceSelector: envResourceRef.ResourceSelector, + }, + } +} + // NewFileSystemResourceRef creates a new file system resource ref. func NewFileSystemResourceRef(path string) ResourceRef { return ResourceRef{ @@ -287,6 +297,17 @@ func NewFileSystemResourceRef(path string) ResourceRef { } } +// NewConfigFileSystemResourceRef creates a new file system resource ref. +func NewFileSystemConfigResourceRef(path string) ConfigRef { + fsResourceRef := NewFileSystemResourceRef(path) + return ConfigRef{ + SelectorType: ConfigResourceSelectorType(fsResourceRef.SelectorType), + ConfigResourceSelector: ConfigResourceSelector{ + ResourceSelector: fsResourceRef.ResourceSelector, + }, + } +} + // NewSecretResourceRef creates a new Secret resource ref. func NewSecretResourceRef(namespace, name, key string) ResourceRef { return ResourceRef{ @@ -301,7 +322,7 @@ func NewSecretResourceRef(namespace, name, key string) ResourceRef { } } -// NewSecretResourceRef creates a new Secret resource ref. +// NewSecretConfigResourceRef creates a new Secret resource ref to be used as config. func NewSecretConfigResourceRef(namespace, name, key string) ConfigRef { secretResourceRef := NewSecretResourceRef(namespace, name, key) return ConfigRef{ @@ -324,6 +345,43 @@ func NewLiteralResourceRef(value string) ResourceRef { } } +// NewLiteralConfigResourceRef creates a new config literal resource ref. +func NewLiteralConfigResourceRef(value string) ConfigRef { + literalResourceRef := NewLiteralResourceRef(value) + return ConfigRef{ + SelectorType: ConfigResourceSelectorType(literalResourceRef.SelectorType), + ConfigResourceSelector: ConfigResourceSelector{ + ResourceSelector: literalResourceRef.ResourceSelector, + }, + } +} + +// NewStructuredConfigResourceRef creates a new structured config resource ref. +func NewStructuredConfigResourceRef(config apiextensionsv1.JSON) ConfigRef { + return ConfigRef{ + SelectorType: ConfigResourceSelectorStructured, + ConfigResourceSelector: ConfigResourceSelector{ + StructuredRef: &StructuredRef{ + Value: config, + }, + }, + } +} + +// NewConfigMapConfigResourceRef creates a new ConfigMap resource ref to be used as config. +func NewConfigMapConfigResourceRef(namespace, name, key string) ConfigRef { + return ConfigRef{ + SelectorType: ConfigResourceSelectorConfigMap, + ConfigResourceSelector: ConfigResourceSelector{ + ConfigMapRef: &ConfigMapSelector{ + Namespace: namespace, + Name: name, + Key: key, + }, + }, + } +} + // ResourceSelectorType identifies the type of the resource reference in type ResourceSelectorType string diff --git a/pkg/controller/stack/stack_config.go b/pkg/controller/stack/stack_config.go index d69665e8..16632e9b 100644 --- a/pkg/controller/stack/stack_config.go +++ b/pkg/controller/stack/stack_config.go @@ -9,18 +9,24 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ) -type StructuredConfig map[string]apiextensionsv1.JSON +type StructuredConfig map[string]any type ConfigKeyValue struct { Key string Value auto.ConfigValue } -func (c StructuredConfig) Unmarshal() ([]ConfigKeyValue, error) { - flatten, err := flattenKeys(c) - if err != nil { +func NewStructuredConfigFromJSON(rawValue apiextensionsv1.JSON) (*StructuredConfig, error) { + var data map[string]any + if err := json.Unmarshal(rawValue.Raw, &data); err != nil { return nil, err } + structuredConfig := StructuredConfig(data) + return &structuredConfig, nil +} + +func (c StructuredConfig) Flatten() []ConfigKeyValue { + flatten := flattenKeys(c) configValues := make([]ConfigKeyValue, 0, len(flatten)) for key, value := range flatten { @@ -32,61 +38,43 @@ func (c StructuredConfig) Unmarshal() ([]ConfigKeyValue, error) { }) } - return configValues, nil + return configValues } -func flattenKeys(config StructuredConfig) (map[string]any, error) { +func flattenKeys(config StructuredConfig) map[string]any { output := make(map[string]any) - for k, jsonValue := range config { - var d any - if err := json.Unmarshal(jsonValue.Raw, &d); err != nil { - return nil, err - } - - err := flatten(output, d, k) - if err != nil { - return nil, err - } + for k, v := range config { + flatten(output, v, k) } - return output, nil + return output } -func flatten(flatMap map[string]any, nested any, prefix string) error { - assign := func(newKey string, v any) error { +func flatten(flatMap map[string]any, nested any, prefix string) { + assign := func(newKey string, v any) { switch v.(type) { case map[string]any, []any: - if err := flatten(flatMap, v, newKey); err != nil { - return err - } + flatten(flatMap, v, newKey) default: flatMap[newKey] = v } - - return nil } switch nested.(type) { case map[string]any: for k, v := range nested.(map[string]any) { newKey := enkey(prefix, k) - if err := assign(newKey, v); err != nil { - return err - } + assign(newKey, v) } case []any: for i, v := range nested.([]any) { newKey := indexedKey(prefix, strconv.Itoa(i)) - if err := assign(newKey, v); err != nil { - return err - } + assign(newKey, v) } default: - return assign(prefix, nested) + assign(prefix, nested) } - - return nil } func enkey(prefix, subkey string) string { diff --git a/pkg/controller/stack/stack_config_test.go b/pkg/controller/stack/stack_config_test.go index 0224d2e6..4b0684c9 100644 --- a/pkg/controller/stack/stack_config_test.go +++ b/pkg/controller/stack/stack_config_test.go @@ -19,32 +19,33 @@ func TestFlattenStackConfigFromJson(t *testing.T) { return apiextensionsv1.JSON{Raw: b} } - sourceConfigMap := map[string]apiextensionsv1.JSON{ - "aws:region": toJson("us-east-1"), - "aws:assumeRole": toJson(map[string]any{ + sourceConfigMap := map[string]any{ + "aws:region": "us-east-1", + "aws:assumeRole": map[string]any{ "roleArn": "my-role-arn", "sessionName": "my-session-name", - }), - "aws:defaultTags": toJson(map[string]any{ + }, + "aws:defaultTags": map[string]any{ "tags": map[string]any{ "my-tag": "tag-value", }, - }), - "an-object-config": toJson(map[string]any{ + }, + "an-object-config": map[string]any{ "another-config": map[string]any{ "config-key": "value", }, "a-nested-list-config": []any{"one", "two", "three"}, - }), - "a-list-config": toJson([]any{"a", "b", "c"}), - "a-simple-config": toJson("just-a-simple-value"), - "a-boolean-config": toJson(true), - "an-integer-config": toJson(123456), + }, + "a-list-config": []any{"a", "b", "c"}, + "a-simple-config": "just-a-simple-value", + "a-boolean-config": true, + "an-integer-config": 123456, } - configValues, err := StructuredConfig(sourceConfigMap).Unmarshal() + structuredConfig, err := NewStructuredConfigFromJSON(toJson(sourceConfigMap)) + if assert.NoError(t, err) { + configValues := structuredConfig.Flatten() - if assert.Nil(t, err) { expected := []ConfigKeyValue{ { Key: "a-boolean-config", @@ -146,6 +147,6 @@ func TestFlattenStackConfigFromJson(t *testing.T) { }, } - assert.Equal(t, expected, configValues) + assert.ElementsMatch(t, expected, configValues) } } diff --git a/pkg/controller/stack/stack_controller.go b/pkg/controller/stack/stack_controller.go index 81ac92fe..50c8abf5 100644 --- a/pkg/controller/stack/stack_controller.go +++ b/pkg/controller/stack/stack_controller.go @@ -1098,26 +1098,24 @@ func (sess *reconcileStackSession) resolveConfigRefs(ctx context.Context) ([]Con if err := sess.kubeClient.Get(ctx, types.NamespacedName{Name: configMapRef.Name, Namespace: configMapRef.Namespace}, &config); err != nil { return nil, fmt.Errorf("Failed to get the ConfigMap %s on namespace %s: %w", configMapRef.Name, configMapRef.Namespace, err) } - allConfigs = append(allConfigs, ConfigKeyValue{ - Key: k, - Value: auto.ConfigValue{ - Value: config.Data[configMapRef.Key], - Secret: false, - }, - }) + // assumes the whole configmaps's data is the config content; try to read as a conventional stack yaml config + var configMapContent map[string]any + if err := yaml.Unmarshal([]byte(config.Data[configMapRef.Key]), &configMapContent); err != nil { + return nil, fmt.Errorf("Failed to read the ConfigMap content as a stack YAML config. Namespace=%s Name=%s: %w", configMapRef.Namespace, configMapRef.Name, err) + } + structuredConfig := StructuredConfig(configMapContent).Flatten() + allConfigs = append(allConfigs, structuredConfig...) } case shared.ConfigResourceSelectorStructured: structuredRef := ref.StructuredRef if structuredRef != nil { // StructuredRef handles value as json, flattening all keys to build a list of Pulumi key:value configs - jsonConfig := StructuredConfig(map[string]apiextensionsv1.JSON{ - k: structuredRef.Value, - }) - structuredConfig, err := jsonConfig.Unmarshal() + structuredConfig, err := NewStructuredConfigFromJSON(structuredRef.Value) if err != nil { return nil, fmt.Errorf("Failed to unmarshall %s as a structured config: %w", k, err) } - allConfigs = append(allConfigs, structuredConfig...) + configs := structuredConfig.Flatten() + allConfigs = append(allConfigs, configs...) } // Secret should be handled here as well because auto.ConfigValue should be marked as Secret:true case shared.ConfigResourceSelectorType(shared.ResourceSelectorSecret): diff --git a/test/stack_controller_test.go b/test/stack_controller_test.go index 86064ddb..2fa3c26a 100644 --- a/test/stack_controller_test.go +++ b/test/stack_controller_test.go @@ -4,6 +4,7 @@ package tests import ( "context" + "encoding/json" "fmt" "os" "os/exec" @@ -14,6 +15,7 @@ import ( "github.com/pulumi/pulumi-kubernetes-operator/pkg/apis/pulumi/shared" pulumiv1 "github.com/pulumi/pulumi-kubernetes-operator/pkg/apis/pulumi/v1" + "sigs.k8s.io/yaml" git "github.com/go-git/go-git/v5" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -205,6 +207,385 @@ var _ = Describe("Stack Controller", func() { }) }) + Context("configuring a stack using ConfigRefs", func() { + var stack *pulumiv1.Stack + var configDir string + + When("using a FileSystemRef", func() { + BeforeEach(func() { + By("Creating directory to store configs") + configDir, err = os.MkdirTemp("", "secrets") + if err != nil { + Fail("Failed to create config temp directory") + } + Expect(os.WriteFile(filepath.Join(configDir, "word.txt"), []byte("just-a-word-in-a-file"), 0600)).To(Succeed()) + }) + + AfterEach(func() { + deleteAndWaitForFinalization(stack) + }) + + It("can deploy a stack reading a config from a file", func() { + + // Use a local backend for this test. + // Local backend doesn't allow setting slashes in stack name. + const stackName = "dev" + fmt.Fprintf(GinkgoWriter, "Stack.Name: %s\n", stackName) + + // Define the stack spec + localSpec := shared.StackSpec{ + Backend: fmt.Sprintf("file://%s", backendDir), + Stack: stackName, + GitSource: &shared.GitSource{ + ProjectRepo: baseDir, + RepoDir: "test/testdata/config-refs", + Commit: commit, + }, + SecretsProvider: "passphrase", + EnvRefs: defaultEnvRefs(), + ConfigRefs: map[string]shared.ConfigRef{ + "word": shared.NewFileSystemConfigResourceRef(filepath.Join(configDir, "word.txt")), + }, + Refresh: true, + } + + // Create the stack + name := "config-refs-with-file-stack" + stack = generateStackV1(name, namespace, localSpec) + Expect(k8sClient.Create(ctx, stack)).Should(Succeed()) + + // Check that the stack updated successfully + fetched := &pulumiv1.Stack{} + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{Name: stack.Name, Namespace: namespace}, fetched) + if err != nil { + return false + } + return stackUpdatedToCommit(fetched.Status.LastUpdate, stack.Spec.Commit) + }, stackExecTimeout, interval).Should(BeTrue()) + // Validate outputs. + Expect(fetched.Status.Outputs).Should(HaveKeyWithValue("word", v1.JSON{Raw: []byte(`"just-a-word-in-a-file"`)})) + }) + }) + + When("using an EnvRef", func() { + AfterEach(func() { + deleteAndWaitForFinalization(stack) + }) + + It("can deploy a stack reading a config from an EnvVar", func() { + + // Use a local backend for this test. + // Local backend doesn't allow setting slashes in stack name. + const stackName = "dev" + fmt.Fprintf(GinkgoWriter, "Stack.Name: %s\n", stackName) + + err := os.Setenv("WORD", "just-a-word") + if err != nil { + Fail("Unable to set WORD environment variable.") + } + + // Define the stack spec + localSpec := shared.StackSpec{ + Backend: fmt.Sprintf("file://%s", backendDir), + Stack: stackName, + GitSource: &shared.GitSource{ + ProjectRepo: baseDir, + RepoDir: "test/testdata/config-refs", + Commit: commit, + }, + SecretsProvider: "passphrase", + EnvRefs: defaultEnvRefs(), + ConfigRefs: map[string]shared.ConfigRef{ + "word": shared.NewEnvConfigResourceRef("WORD"), + }, + Refresh: true, + } + + // Create the stack + name := "config-refs-with-envs-stack" + stack = generateStackV1(name, namespace, localSpec) + Expect(k8sClient.Create(ctx, stack)).Should(Succeed()) + + // Check that the stack updated successfully + fetched := &pulumiv1.Stack{} + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{Name: stack.Name, Namespace: namespace}, fetched) + if err != nil { + return false + } + return stackUpdatedToCommit(fetched.Status.LastUpdate, stack.Spec.Commit) + }, stackExecTimeout, interval).Should(BeTrue()) + // Validate outputs. + Expect(fetched.Status.Outputs).Should(HaveKeyWithValue("word", v1.JSON{Raw: []byte(`"just-a-word"`)})) + }) + }) + + When("using a SecretRef", func() { + var configSecret *corev1.Secret + + BeforeEach(func() { + // Create the config secret + By("Creating the Config Secret") + configSecret = generateSecret("config-secret", namespace, + map[string][]byte{ + "secret-word": []byte("just-a-secret-word"), + }, + ) + Expect(k8sClient.Create(ctx, configSecret)).Should(Succeed()) + DeferCleanup(func() { + if configSecret != nil { + By("Deleting the Config Secret") + Expect(k8sClient.Delete(ctx, configSecret)).Should(Succeed()) + } + }) + }) + + AfterEach(func() { + deleteAndWaitForFinalization(stack) + }) + + It("can deploy a stack reading a config from a Secret", func() { + + // Use a local backend for this test. + // Local backend doesn't allow setting slashes in stack name. + const stackName = "dev" + fmt.Fprintf(GinkgoWriter, "Stack.Name: %s\n", stackName) + + // Define the stack spec + localSpec := shared.StackSpec{ + Backend: fmt.Sprintf("file://%s", backendDir), + Stack: stackName, + GitSource: &shared.GitSource{ + ProjectRepo: baseDir, + RepoDir: "test/testdata/config-refs", + Commit: commit, + }, + SecretsProvider: "passphrase", + EnvRefs: defaultEnvRefs(), + Config: map[string]string{ + "word": "just-a-word", + }, + ConfigRefs: map[string]shared.ConfigRef{ + "secret-word": shared.NewSecretConfigResourceRef(namespace, configSecret.Name, "secret-word"), + }, + Refresh: true, + } + + // Create the stack + name := "config-refs-with-secret-stack" + stack = generateStackV1(name, namespace, localSpec) + Expect(k8sClient.Create(ctx, stack)).Should(Succeed()) + + // Check that the stack updated successfully + fetched := &pulumiv1.Stack{} + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{Name: stack.Name, Namespace: namespace}, fetched) + if err != nil { + return false + } + return stackUpdatedToCommit(fetched.Status.LastUpdate, stack.Spec.Commit) + }, stackExecTimeout, interval).Should(BeTrue()) + // Validate outputs. + Expect(fetched.Status.Outputs).Should(BeEquivalentTo(shared.StackOutputs{ + "word": v1.JSON{Raw: []byte(`"just-a-word"`)}, + "secret-word": v1.JSON{Raw: []byte(`"[secret]"`)}, + })) + }) + }) + + When("using a LiteralRef", func() { + AfterEach(func() { + deleteAndWaitForFinalization(stack) + }) + + It("can deploy a stack reading a config from a Literal value", func() { + + // Use a local backend for this test. + // Local backend doesn't allow setting slashes in stack name. + const stackName = "dev" + fmt.Fprintf(GinkgoWriter, "Stack.Name: %s\n", stackName) + + // Define the stack spec + localSpec := shared.StackSpec{ + Backend: fmt.Sprintf("file://%s", backendDir), + Stack: stackName, + GitSource: &shared.GitSource{ + ProjectRepo: baseDir, + RepoDir: "test/testdata/config-refs", + Commit: commit, + }, + SecretsProvider: "passphrase", + EnvRefs: defaultEnvRefs(), + ConfigRefs: map[string]shared.ConfigRef{ + "word": shared.NewLiteralConfigResourceRef("just-a-literal-word"), + }, + Refresh: true, + } + + // Create the stack + name := "config-refs-with-literal-stack" + stack = generateStackV1(name, namespace, localSpec) + Expect(k8sClient.Create(ctx, stack)).Should(Succeed()) + + // Check that the stack updated successfully + fetched := &pulumiv1.Stack{} + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{Name: stack.Name, Namespace: namespace}, fetched) + if err != nil { + return false + } + return stackUpdatedToCommit(fetched.Status.LastUpdate, stack.Spec.Commit) + }, stackExecTimeout, interval).Should(BeTrue()) + // Validate outputs. + Expect(fetched.Status.Outputs).Should(HaveKeyWithValue("word", v1.JSON{Raw: []byte(`"just-a-literal-word"`)})) + }) + }) + + When("using a StructuredRef", func() { + AfterEach(func() { + deleteAndWaitForFinalization(stack) + }) + + It("can deploy a stack reading a config from a Structured value", func() { + + // Use a local backend for this test. + // Local backend doesn't allow setting slashes in stack name. + const stackName = "dev" + fmt.Fprintf(GinkgoWriter, "Stack.Name: %s\n", stackName) + + structuredConfig := map[string]any{ + "structured": map[string]any{ + "nested": map[string]any{ + "field": "just-a-structured-value", + }, + }, + } + jsonStructuredConfig, err := json.Marshal(structuredConfig) + if err != nil { + Fail("Failed to serialize a structured config to json.") + } + + // Define the stack spec + localSpec := shared.StackSpec{ + Backend: fmt.Sprintf("file://%s", backendDir), + Stack: stackName, + GitSource: &shared.GitSource{ + ProjectRepo: baseDir, + RepoDir: "test/testdata/structured-config-refs", + Commit: commit, + }, + SecretsProvider: "passphrase", + EnvRefs: defaultEnvRefs(), + ConfigRefs: map[string]shared.ConfigRef{ + "structured": shared.NewStructuredConfigResourceRef(v1.JSON{Raw: jsonStructuredConfig}), + }, + Refresh: true, + } + + // Create the stack + name := "config-refs-with-literal-stack" + stack = generateStackV1(name, namespace, localSpec) + Expect(k8sClient.Create(ctx, stack)).Should(Succeed()) + + // Check that the stack updated successfully + fetched := &pulumiv1.Stack{} + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{Name: stack.Name, Namespace: namespace}, fetched) + if err != nil { + return false + } + return stackUpdatedToCommit(fetched.Status.LastUpdate, stack.Spec.Commit) + }, stackExecTimeout, interval).Should(BeTrue()) + // Validate outputs. + Expect(fetched.Status.Outputs).Should(BeEquivalentTo(shared.StackOutputs{ + "nested-config-field": v1.JSON{Raw: []byte(`"just-a-structured-value"`)}, + })) + }) + }) + + When("using a ConfigMapRef", func() { + var configMap *corev1.ConfigMap + + BeforeEach(func() { + // Create the configmap + By("Creating the ConfigMap") + + structuredConfig := map[string]any{ + "structured": map[string]any{ + "nested": map[string]any{ + "field": "just-a-structured-value", + }, + }, + } + + structuredConfigAsYaml, err := yaml.Marshal(structuredConfig) + if err != nil { + Fail("Failed to serialize a structured config to yaml.") + } + + configMap = generateConfigMap("config-secret", namespace, map[string]string{ + "Pulumi.dev.yaml": string(structuredConfigAsYaml), + }) + Expect(k8sClient.Create(ctx, configMap)).Should(Succeed()) + DeferCleanup(func() { + if configMap != nil { + By("Deleting the ConfigMap") + Expect(k8sClient.Delete(ctx, configMap)).Should(Succeed()) + } + }) + }) + + AfterEach(func() { + deleteAndWaitForFinalization(stack) + }) + + It("can deploy a stack reading a config from a ConfigMap", func() { + + // Use a local backend for this test. + // Local backend doesn't allow setting slashes in stack name. + const stackName = "dev" + fmt.Fprintf(GinkgoWriter, "Stack.Name: %s\n", stackName) + + // Define the stack spec + localSpec := shared.StackSpec{ + Backend: fmt.Sprintf("file://%s", backendDir), + Stack: stackName, + GitSource: &shared.GitSource{ + ProjectRepo: baseDir, + RepoDir: "test/testdata/structured-config-refs", + Commit: commit, + }, + SecretsProvider: "passphrase", + EnvRefs: defaultEnvRefs(), + ConfigRefs: map[string]shared.ConfigRef{ + "stack-config": shared.NewConfigMapConfigResourceRef(namespace, configMap.Name, "Pulumi.dev.yaml"), + }, + Refresh: true, + } + + // Create the stack + name := "config-refs-with-literal-stack" + stack = generateStackV1(name, namespace, localSpec) + Expect(k8sClient.Create(ctx, stack)).Should(Succeed()) + + // Check that the stack updated successfully + fetched := &pulumiv1.Stack{} + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{Name: stack.Name, Namespace: namespace}, fetched) + if err != nil { + return false + } + return stackUpdatedToCommit(fetched.Status.LastUpdate, stack.Spec.Commit) + }, stackExecTimeout, interval).Should(BeTrue()) + // Validate outputs. + Expect(fetched.Status.Outputs).Should(BeEquivalentTo(shared.StackOutputs{ + "nested-config-field": v1.JSON{Raw: []byte(`"just-a-structured-value"`)}, + })) + }) + }) + }) + Context("Using the AWS provider", func() { var stack *pulumiv1.Stack @@ -494,3 +875,17 @@ func generateSecret(name, namespace string, data map[string][]byte) *corev1.Secr Type: "Opaque", } } + +func generateConfigMap(name, namespace string, data map[string]string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: strings.Join([]string{name, randString()}, "-"), + Namespace: namespace, + }, + Data: data, + } +}