From 6eb44956c34f6d476789a2b925d06bfe749ad3f7 Mon Sep 17 00:00:00 2001 From: Eron Wright Date: Wed, 17 Apr 2024 10:18:48 -0700 Subject: [PATCH] [dotnet] Unknowns for previews involving an uninitialized provider (#2957) ### Proposed changes This PR fixes the "panic" that occurs when an uninitialized provider is used with the yaml/kustomize resources. In that situation, the invoke calls aren't performed and the engine simply returns an uninitialized result. This causes a problem in the callers because the methods on `ImmutableArray` mustn't be used when the value is uninitialized (`IsDefault` is true). ### Example The example given in #2741 easily repros the issue. With this fix, the behavior improves: ``` Diagnostics: kubernetes:yaml:ConfigFile (guestbook): warning: Required input properties have unknown values. Preview is incomplete. Outputs: FrontendIp: output ``` ### Testing Includes integration tests involving an uninitialized provider with a `ConfigFile` or `Directory`. ### Related issues (optional) Closes #2741 --- CHANGELOG.md | 3 ++ .../gen/dotnet-templates/helm/v3/Invokes.cs | 25 ++++----- .../gen/dotnet-templates/kustomize/Invokes.cs | 16 ++++-- .../pkg/gen/dotnet-templates/yaml/Invokes.cs | 16 ++++-- sdk/dotnet/Helm/V3/Invokes.cs | 25 ++++----- sdk/dotnet/Kustomize/Invokes.cs | 16 ++++-- sdk/dotnet/Yaml/Invokes.cs | 16 ++++-- tests/sdk/dotnet/dotnet_test.go | 54 ++++++++++++++++++- .../KustomizeUninitializedProvider.csproj | 8 +++ .../Program.cs | 35 ++++++++++++ .../Pulumi.yaml | 3 ++ .../kustomize/kustomization.yaml | 5 ++ .../kustomize/manifest.yaml | 7 +++ .../yaml-uninitialized-provider/Program.cs | 35 ++++++++++++ .../yaml-uninitialized-provider/Pulumi.yaml | 3 ++ .../YamlUninitializedProvider.csproj | 8 +++ .../yaml-uninitialized-provider/manifest.yaml | 7 +++ 17 files changed, 244 insertions(+), 38 deletions(-) create mode 100644 tests/sdk/dotnet/kustomize-uninitialized-provider/KustomizeUninitializedProvider.csproj create mode 100644 tests/sdk/dotnet/kustomize-uninitialized-provider/Program.cs create mode 100644 tests/sdk/dotnet/kustomize-uninitialized-provider/Pulumi.yaml create mode 100644 tests/sdk/dotnet/kustomize-uninitialized-provider/kustomize/kustomization.yaml create mode 100644 tests/sdk/dotnet/kustomize-uninitialized-provider/kustomize/manifest.yaml create mode 100644 tests/sdk/dotnet/yaml-uninitialized-provider/Program.cs create mode 100644 tests/sdk/dotnet/yaml-uninitialized-provider/Pulumi.yaml create mode 100644 tests/sdk/dotnet/yaml-uninitialized-provider/YamlUninitializedProvider.csproj create mode 100644 tests/sdk/dotnet/yaml-uninitialized-provider/manifest.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 2be9dafbae..40d12fde6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## Unreleased +- [dotnet] Unknowns for previews involving an uninitialized provider (https://github.com/pulumi/pulumi-kubernetes/pull/2957) + ## 4.10.0 (April 11, 2024) - ConfigGroup V2 (https://github.com/pulumi/pulumi-kubernetes/pull/2844) diff --git a/provider/pkg/gen/dotnet-templates/helm/v3/Invokes.cs b/provider/pkg/gen/dotnet-templates/helm/v3/Invokes.cs index 5a8e5a49bd..7dce86478a 100644 --- a/provider/pkg/gen/dotnet-templates/helm/v3/Invokes.cs +++ b/provider/pkg/gen/dotnet-templates/helm/v3/Invokes.cs @@ -25,18 +25,19 @@ internal static class Invokes /// Invoke the resource provider to fetch a Helm Chart, expand it into YAML, and return the corresponding objects. /// internal static Output>> HelmTemplate(HelmTemplateArgs args, - InvokeOptions? options = null) - => Output.Create(Deployment.Instance.InvokeAsync("kubernetes:helm:template", args, - options.WithDefaults())).Apply(r => - { - /// Invoke on an unconfigured provider results in an empty response. - /// TODO Change this based on how https://github.com/pulumi/pulumi/issues/10209 - /// is addressed. - if (r.Result.IsDefaultOrEmpty) { - return ImmutableArray.Create>(); - } - return r.Result; - }); + InvokeOptions? options = null) { + Output>> Convert(HelmTemplateResult r) { + var a = r.Result; + if (a.IsDefault) { + Pulumi.Log.Warn("The provider is not fully configured. Preview is incomplete.", options?.Parent); + return Pulumi.Utilities.OutputUtilities.CreateUnknown(a); + } + return Pulumi.Output.Create(a); + } + + return Output.Create(Deployment.Instance.InvokeAsync("kubernetes:helm:template", args, + options.WithDefaults())).Apply(Convert); + } } internal class HelmTemplateArgs : InvokeArgs diff --git a/provider/pkg/gen/dotnet-templates/kustomize/Invokes.cs b/provider/pkg/gen/dotnet-templates/kustomize/Invokes.cs index 43222ea5db..210fdbf952 100644 --- a/provider/pkg/gen/dotnet-templates/kustomize/Invokes.cs +++ b/provider/pkg/gen/dotnet-templates/kustomize/Invokes.cs @@ -25,9 +25,19 @@ internal static class Invokes /// Invoke the resource provider to process a kustomization. /// internal static Output>> KustomizeDirectory(KustomizeDirectoryArgs args, - InvokeOptions? options = null) - => Output.Create(Deployment.Instance.InvokeAsync("kubernetes:kustomize:directory", args, - options.WithDefaults())).Apply(r => r.Result.ToImmutableArray()); + InvokeOptions? options = null) { + Output>> Convert(KustomizeDirectoryResult r) { + var a = r.Result; + if (a.IsDefault) { + Pulumi.Log.Warn("The provider is not fully configured. Preview is incomplete.", options?.Parent); + return Pulumi.Utilities.OutputUtilities.CreateUnknown(a); + } + return Pulumi.Output.Create(a); + } + + return Output.Create(Deployment.Instance.InvokeAsync("kubernetes:kustomize:directory", args, + options.WithDefaults())).Apply(Convert); + } } internal class KustomizeDirectoryArgs : InvokeArgs diff --git a/provider/pkg/gen/dotnet-templates/yaml/Invokes.cs b/provider/pkg/gen/dotnet-templates/yaml/Invokes.cs index 3f7db3b993..4cae707354 100644 --- a/provider/pkg/gen/dotnet-templates/yaml/Invokes.cs +++ b/provider/pkg/gen/dotnet-templates/yaml/Invokes.cs @@ -25,9 +25,19 @@ internal static class Invokes /// Invoke the resource provider to decode a YAML string. /// internal static Output>> YamlDecode(YamlDecodeArgs args, - InvokeOptions? options = null) - => Output.Create(Deployment.Instance.InvokeAsync("kubernetes:yaml:decode", args, - options.WithDefaults())).Apply(r => r.Result.ToImmutableArray()); + InvokeOptions? options = null) { + Output>> Convert(YamlDecodeResult r) { + var a = r.Result; + if (a.IsDefault) { + Pulumi.Log.Warn("The provider is not fully configured. Preview is incomplete.", options?.Parent); + return Pulumi.Utilities.OutputUtilities.CreateUnknown(a); + } + return Pulumi.Output.Create(a); + } + + return Output.Create(Deployment.Instance.InvokeAsync("kubernetes:yaml:decode", args, + options.WithDefaults())).Apply(Convert); + } } internal class YamlDecodeArgs : InvokeArgs diff --git a/sdk/dotnet/Helm/V3/Invokes.cs b/sdk/dotnet/Helm/V3/Invokes.cs index 5a8e5a49bd..7dce86478a 100644 --- a/sdk/dotnet/Helm/V3/Invokes.cs +++ b/sdk/dotnet/Helm/V3/Invokes.cs @@ -25,18 +25,19 @@ internal static class Invokes /// Invoke the resource provider to fetch a Helm Chart, expand it into YAML, and return the corresponding objects. /// internal static Output>> HelmTemplate(HelmTemplateArgs args, - InvokeOptions? options = null) - => Output.Create(Deployment.Instance.InvokeAsync("kubernetes:helm:template", args, - options.WithDefaults())).Apply(r => - { - /// Invoke on an unconfigured provider results in an empty response. - /// TODO Change this based on how https://github.com/pulumi/pulumi/issues/10209 - /// is addressed. - if (r.Result.IsDefaultOrEmpty) { - return ImmutableArray.Create>(); - } - return r.Result; - }); + InvokeOptions? options = null) { + Output>> Convert(HelmTemplateResult r) { + var a = r.Result; + if (a.IsDefault) { + Pulumi.Log.Warn("The provider is not fully configured. Preview is incomplete.", options?.Parent); + return Pulumi.Utilities.OutputUtilities.CreateUnknown(a); + } + return Pulumi.Output.Create(a); + } + + return Output.Create(Deployment.Instance.InvokeAsync("kubernetes:helm:template", args, + options.WithDefaults())).Apply(Convert); + } } internal class HelmTemplateArgs : InvokeArgs diff --git a/sdk/dotnet/Kustomize/Invokes.cs b/sdk/dotnet/Kustomize/Invokes.cs index 43222ea5db..210fdbf952 100644 --- a/sdk/dotnet/Kustomize/Invokes.cs +++ b/sdk/dotnet/Kustomize/Invokes.cs @@ -25,9 +25,19 @@ internal static class Invokes /// Invoke the resource provider to process a kustomization. /// internal static Output>> KustomizeDirectory(KustomizeDirectoryArgs args, - InvokeOptions? options = null) - => Output.Create(Deployment.Instance.InvokeAsync("kubernetes:kustomize:directory", args, - options.WithDefaults())).Apply(r => r.Result.ToImmutableArray()); + InvokeOptions? options = null) { + Output>> Convert(KustomizeDirectoryResult r) { + var a = r.Result; + if (a.IsDefault) { + Pulumi.Log.Warn("The provider is not fully configured. Preview is incomplete.", options?.Parent); + return Pulumi.Utilities.OutputUtilities.CreateUnknown(a); + } + return Pulumi.Output.Create(a); + } + + return Output.Create(Deployment.Instance.InvokeAsync("kubernetes:kustomize:directory", args, + options.WithDefaults())).Apply(Convert); + } } internal class KustomizeDirectoryArgs : InvokeArgs diff --git a/sdk/dotnet/Yaml/Invokes.cs b/sdk/dotnet/Yaml/Invokes.cs index 3f7db3b993..4cae707354 100644 --- a/sdk/dotnet/Yaml/Invokes.cs +++ b/sdk/dotnet/Yaml/Invokes.cs @@ -25,9 +25,19 @@ internal static class Invokes /// Invoke the resource provider to decode a YAML string. /// internal static Output>> YamlDecode(YamlDecodeArgs args, - InvokeOptions? options = null) - => Output.Create(Deployment.Instance.InvokeAsync("kubernetes:yaml:decode", args, - options.WithDefaults())).Apply(r => r.Result.ToImmutableArray()); + InvokeOptions? options = null) { + Output>> Convert(YamlDecodeResult r) { + var a = r.Result; + if (a.IsDefault) { + Pulumi.Log.Warn("The provider is not fully configured. Preview is incomplete.", options?.Parent); + return Pulumi.Utilities.OutputUtilities.CreateUnknown(a); + } + return Pulumi.Output.Create(a); + } + + return Output.Create(Deployment.Instance.InvokeAsync("kubernetes:yaml:decode", args, + options.WithDefaults())).Apply(Convert); + } } internal class YamlDecodeArgs : InvokeArgs diff --git a/tests/sdk/dotnet/dotnet_test.go b/tests/sdk/dotnet/dotnet_test.go index b41f86c04c..0e06853b99 100644 --- a/tests/sdk/dotnet/dotnet_test.go +++ b/tests/sdk/dotnet/dotnet_test.go @@ -110,6 +110,23 @@ func TestDotnet_YamlLocal(t *testing.T) { integration.ProgramTest(t, &test) } +func TestDotnet_YamlUninitializedProvider(t *testing.T) { + test := baseOptions.With(integration.ProgramTestOptions{ + Dir: "yaml-uninitialized-provider", + Quick: false, + SkipPreview: false, + SkipExportImport: true, + SkipEmptyPreviewUpdate: true, + SkipUpdate: true, + SkipRefresh: true, + ExpectRefreshChanges: true, + AllowEmptyPreviewChanges: true, + }) + integration.ProgramTest(t, &test) + + // FUTURE: verify that the stack outputs include 'serviceUid' and has an unknown value. +} + func TestDotnet_Helm(t *testing.T) { test := baseOptions.With(integration.ProgramTestOptions{ Dir: filepath.Join("helm", "step1"), @@ -262,6 +279,23 @@ func TestDotnet_Kustomize(t *testing.T) { integration.ProgramTest(t, &test) } +func TestDotnet_Kustomize_UninitializedProvider(t *testing.T) { + test := baseOptions.With(integration.ProgramTestOptions{ + Dir: "kustomize-uninitialized-provider", + Quick: false, + SkipPreview: false, + SkipExportImport: true, + SkipEmptyPreviewUpdate: true, + SkipUpdate: true, + SkipRefresh: true, + ExpectRefreshChanges: true, + AllowEmptyPreviewChanges: true, + }) + integration.ProgramTest(t, &test) + + // FUTURE: verify that the stack outputs include 'serviceUid' and has an unknown value. +} + func TestDotnet_Secrets(t *testing.T) { secretMessage := "secret message for testing" @@ -311,10 +345,10 @@ func TestDotnet_ServerSideApply(t *testing.T) { integration.ProgramTest(t, &test) } -// TestOptionPropagation tests the handling of resource options by the various compoonent resources. +// TestDotnet_OptionPropagation tests the handling of resource options by the various compoonent resources. // Component resources are responsible for implementing option propagation logic when creating // child resources. -func TestOptionPropagation(t *testing.T) { +func TestDotnet_OptionPropagation(t *testing.T) { g := NewWithT(t) format.MaxLength = 0 format.MaxDepth = 5 @@ -803,3 +837,19 @@ func TestOptionPropagation(t *testing.T) { err = pt.TestPreviewUpdateAndEdits() require.NoError(t, err) } + +func TestYamlUninitializedProvider(t *testing.T) { + test := baseOptions.With(integration.ProgramTestOptions{ + Dir: "yaml-uninitialized-provider", + Quick: false, + SkipPreview: false, + SkipExportImport: true, + SkipUpdate: true, + SkipRefresh: true, + ExpectRefreshChanges: true, + AllowEmptyPreviewChanges: true, + }) + integration.ProgramTest(t, &test) + + // FUTURE: verify that the stack outputs include 'serviceUid' and has an unknown value. +} diff --git a/tests/sdk/dotnet/kustomize-uninitialized-provider/KustomizeUninitializedProvider.csproj b/tests/sdk/dotnet/kustomize-uninitialized-provider/KustomizeUninitializedProvider.csproj new file mode 100644 index 0000000000..01b2133c80 --- /dev/null +++ b/tests/sdk/dotnet/kustomize-uninitialized-provider/KustomizeUninitializedProvider.csproj @@ -0,0 +1,8 @@ + + + + Exe + net6.0 + + + diff --git a/tests/sdk/dotnet/kustomize-uninitialized-provider/Program.cs b/tests/sdk/dotnet/kustomize-uninitialized-provider/Program.cs new file mode 100644 index 0000000000..b80ae6abf5 --- /dev/null +++ b/tests/sdk/dotnet/kustomize-uninitialized-provider/Program.cs @@ -0,0 +1,35 @@ +using Kubernetes = Pulumi.Kubernetes; +using Kustomize = Pulumi.Kubernetes.Kustomize; +using CoreV1 = Pulumi.Kubernetes.Core.V1; +using Pulumi; +using System.Collections.Generic; +using System.IO; + +return await Deployment.RunAsync(() => +{ + // Create an uninitialized provider + var provider = new Kubernetes.Provider("provider", new() + { + KubeConfig = Pulumi.Utilities.OutputUtilities.CreateUnknown(""), + }); + + // Create resources using Directory (and for which Invoke is skipped) + var directory = new Kustomize.Directory("directory", + new Kustomize.DirectoryArgs + { + Directory = "kustomize", + }, + new ComponentResourceOptions + { + Provider = provider + }); + + // Lookup the registered service, to exercise the 'resources' output property. + // During preview, we expect the stack outputs to be unknown. + var service = directory.GetResource("kustomize-uninitialized-provider"); + + return new Dictionary + { + ["serviceUid"] = service.Apply(svc => svc.Metadata.Apply(meta => meta.Uid)), + }; +}); \ No newline at end of file diff --git a/tests/sdk/dotnet/kustomize-uninitialized-provider/Pulumi.yaml b/tests/sdk/dotnet/kustomize-uninitialized-provider/Pulumi.yaml new file mode 100644 index 0000000000..cbcd1460f5 --- /dev/null +++ b/tests/sdk/dotnet/kustomize-uninitialized-provider/Pulumi.yaml @@ -0,0 +1,3 @@ +name: dotnet_kustomize_uninitialized_provider +description: A test of the kustomize resoures with an uninitialized provider +runtime: dotnet diff --git a/tests/sdk/dotnet/kustomize-uninitialized-provider/kustomize/kustomization.yaml b/tests/sdk/dotnet/kustomize-uninitialized-provider/kustomize/kustomization.yaml new file mode 100644 index 0000000000..b785f99642 --- /dev/null +++ b/tests/sdk/dotnet/kustomize-uninitialized-provider/kustomize/kustomization.yaml @@ -0,0 +1,5 @@ +commonLabels: + app: hello + +resources: +- manifest.yaml diff --git a/tests/sdk/dotnet/kustomize-uninitialized-provider/kustomize/manifest.yaml b/tests/sdk/dotnet/kustomize-uninitialized-provider/kustomize/manifest.yaml new file mode 100644 index 0000000000..3b2fb3bc5e --- /dev/null +++ b/tests/sdk/dotnet/kustomize-uninitialized-provider/kustomize/manifest.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Service +metadata: + name: kustomize-uninitialized-provider +spec: + type: ExternalName + externalName: localhost diff --git a/tests/sdk/dotnet/yaml-uninitialized-provider/Program.cs b/tests/sdk/dotnet/yaml-uninitialized-provider/Program.cs new file mode 100644 index 0000000000..703460adca --- /dev/null +++ b/tests/sdk/dotnet/yaml-uninitialized-provider/Program.cs @@ -0,0 +1,35 @@ +using Kubernetes = Pulumi.Kubernetes; +using Yaml = Pulumi.Kubernetes.Yaml; +using CoreV1 = Pulumi.Kubernetes.Core.V1; +using Pulumi; +using System.Collections.Generic; +using System.IO; + +return await Deployment.RunAsync(() => +{ + // Create an uninitialized provider + var provider = new Kubernetes.Provider("provider", new() + { + KubeConfig = Pulumi.Utilities.OutputUtilities.CreateUnknown(""), + }); + + // Create resources using ConfigFile (and for which Invoke is skipped) + var manifest = new Yaml.ConfigFile("manifest", + new Yaml.ConfigFileArgs + { + File = "manifest.yaml", + }, + new ComponentResourceOptions + { + Provider = provider + }); + + // Lookup the registered service, to exercise the 'resources' output property. + // During preview, we expect the stack outputs to be unknown. + var service = manifest.GetResource("yaml-uninitialized-provider"); + + return new Dictionary + { + ["serviceUid"] = service.Apply(svc => svc.Metadata.Apply(meta => meta.Uid)), + }; +}); \ No newline at end of file diff --git a/tests/sdk/dotnet/yaml-uninitialized-provider/Pulumi.yaml b/tests/sdk/dotnet/yaml-uninitialized-provider/Pulumi.yaml new file mode 100644 index 0000000000..196f1c897d --- /dev/null +++ b/tests/sdk/dotnet/yaml-uninitialized-provider/Pulumi.yaml @@ -0,0 +1,3 @@ +name: dotnet_yaml_uninitialized_provider +description: A test of the yaml resoures with an uninitialized provider +runtime: dotnet diff --git a/tests/sdk/dotnet/yaml-uninitialized-provider/YamlUninitializedProvider.csproj b/tests/sdk/dotnet/yaml-uninitialized-provider/YamlUninitializedProvider.csproj new file mode 100644 index 0000000000..01b2133c80 --- /dev/null +++ b/tests/sdk/dotnet/yaml-uninitialized-provider/YamlUninitializedProvider.csproj @@ -0,0 +1,8 @@ + + + + Exe + net6.0 + + + diff --git a/tests/sdk/dotnet/yaml-uninitialized-provider/manifest.yaml b/tests/sdk/dotnet/yaml-uninitialized-provider/manifest.yaml new file mode 100644 index 0000000000..0293bb0d61 --- /dev/null +++ b/tests/sdk/dotnet/yaml-uninitialized-provider/manifest.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Service +metadata: + name: yaml-uninitialized-provider +spec: + type: ExternalName + externalName: localhost