Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Make the invoke calls for Helm charts and YAML config resilient to the value being None or an empty dict #2665

Merged
merged 7 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Unreleased

- Fix: Make the invoke calls for Helm charts and YAML config resilient to the value being None or an empty dict (https://github.com/pulumi/pulumi-kubernetes/pull/2665)

## 4.5.4 (November 8, 2023)
- Fix: Helm Release: chart requires kubeVersion (https://github.com/pulumi/pulumi-kubernetes/pull/2653)

Expand Down
2 changes: 1 addition & 1 deletion provider/pkg/gen/python-templates/helm/v3/helm.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,6 @@ def _parse_chart(all_config: Tuple[Union[ChartOpts, LocalChartOpts], pulumi.Reso

def invoke_helm_template(opts):
inv = pulumi.runtime.invoke('kubernetes:helm:template', {'jsonOpts': opts}, invoke_opts)
return inv.value['result'] if inv is not None and inv.value is not None else []
return (inv.value or {}).get('result', [])
objects = json_opts.apply(invoke_helm_template)
return objects.apply(lambda x: _parse_yaml_document(x, opts, transformations))
2 changes: 1 addition & 1 deletion provider/pkg/gen/python-templates/yaml/yaml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -517,4 +517,4 @@ def _parse_yaml_object(

def invoke_yaml_decode(text, invoke_opts):
inv = pulumi.runtime.invoke('kubernetes:yaml:decode', {'text': text}, invoke_opts)
return inv.value['result'] if inv is not None and inv.value is not None else []
return (inv.value or {}).get('result', [])
2 changes: 1 addition & 1 deletion sdk/python/pulumi_kubernetes/helm/v3/helm.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,6 @@ def _parse_chart(all_config: Tuple[Union[ChartOpts, LocalChartOpts], pulumi.Reso

def invoke_helm_template(opts):
inv = pulumi.runtime.invoke('kubernetes:helm:template', {'jsonOpts': opts}, invoke_opts)
return inv.value['result'] if inv is not None and inv.value is not None else []
return (inv.value or {}).get('result', [])
objects = json_opts.apply(invoke_helm_template)
return objects.apply(lambda x: _parse_yaml_document(x, opts, transformations))
2 changes: 1 addition & 1 deletion sdk/python/pulumi_kubernetes/yaml/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -1949,4 +1949,4 @@ def _parse_yaml_object(

def invoke_yaml_decode(text, invoke_opts):
inv = pulumi.runtime.invoke('kubernetes:yaml:decode', {'text': text}, invoke_opts)
return inv.value['result'] if inv is not None and inv.value is not None else []
return (inv.value or {}).get('result', [])
3 changes: 3 additions & 0 deletions tests/sdk/python/helm-local-unconfigured-provider/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: helm-local-unconfigured-provider
description: A program that tests Helm chart creation from a local directory with an unconfigured provider
runtime: python
56 changes: 56 additions & 0 deletions tests/sdk/python/helm-local-unconfigured-provider/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright 2016-2023, 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.

from typing import Any

from pulumi_kubernetes import Provider
from pulumi_kubernetes.core.v1 import Namespace
from pulumi_kubernetes.helm.v3 import Chart, LocalChartOpts
from pulumi import Config, ResourceOptions


def set_namespace(namespace):
def f(obj: Any):
if "metadata" in obj:
obj["metadata"]["namespace"] = namespace.metadata["name"]
else:
obj["metadata"] = {"namespace": namespace.metadata["name"]}

return f


config = Config()
path = config.require("path")

# This will be unknown during the initial preview.
unknown = Provider("provider").id.apply(lambda _: True)

# This provider will be unconfigured when the passed-in configuration has an unknown value.
provider = Provider("k8s", suppress_deprecation_warnings=unknown)

values = {"service": {"type": "ClusterIP"}}

ns = Namespace("unconfiguredtest", opts=ResourceOptions(provider=provider))

# An error shouldn't be raised when called using the unconfigured provider.
chart = Chart(
"nginx",
LocalChartOpts(
path=path,
namespace=ns.metadata.name,
values=values,
transformations=[set_namespace(ns)],
),
opts=ResourceOptions(provider=provider)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pulumi>=3.0.0,<4.0.0
3 changes: 3 additions & 0 deletions tests/sdk/python/kustomize-unconfigured-provider/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: kustomize-test-unconfigured-provider
description: Test kustomize support using an unconfigured provider
runtime: python
45 changes: 45 additions & 0 deletions tests/sdk/python/kustomize-unconfigured-provider/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright 2016-2023, 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.

import pulumi_kubernetes as k8s
from pulumi import ResourceOptions

from typing import Any


def set_namespace(namespace):
def f(obj: Any):
if "metadata" in obj:
obj["metadata"]["namespace"] = namespace.metadata["name"]
else:
obj["metadata"] = {"namespace": namespace.metadata["name"]}

return f


# This will be unknown during the initial preview.
unknown = k8s.Provider("provider").id.apply(lambda _: True)

# This provider will be unconfigured when the passed-in configuration has an unknown value.
provider = k8s.Provider("k8s", suppress_deprecation_warnings=unknown)

ns = k8s.core.v1.Namespace("unconfiguredtest", opts=ResourceOptions(provider=provider))

# An error shouldn't be raised when called using the unconfigured provider.
k8s.kustomize.Directory(
"kustomize-local",
"helloWorld",
transformations=[set_namespace(ns)],
opts=ResourceOptions(provider=provider),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: the-map
data:
altGreeting: "Good Morning!"
enableRisky: "false"
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: the-deployment
spec:
replicas: 3
selector:
matchLabels:
deployment: hello
template:
metadata:
labels:
deployment: hello
spec:
containers:
- name: the-container
image: monopole/hello:1
command: ["/hello",
"--port=8080",
"--enableRiskyFeature=$(ENABLE_RISKY)"]
ports:
- containerPort: 8080
env:
- name: ALT_GREETING
valueFrom:
configMapKeyRef:
name: the-map
key: altGreeting
- name: ENABLE_RISKY
valueFrom:
configMapKeyRef:
name: the-map
key: enableRisky
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Example configuration for the webserver
# at https://github.com/monopole/hello
commonLabels:
app: hello

resources:
- deployment.yaml
- service.yaml
- configMap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
kind: Service
apiVersion: v1
metadata:
name: the-service
spec:
selector:
deployment: hello
type: LoadBalancer
ports:
- protocol: TCP
port: 8666
targetPort: 8080
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pulumi>=3.0.0,<4.0.0
69 changes: 69 additions & 0 deletions tests/sdk/python/python_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,28 @@ func TestYaml(t *testing.T) {
integration.ProgramTest(t, &options)
}

// Regression Test for https://github.com/pulumi/pulumi-kubernetes/issues/2664.
// Ensure the program runs without an error being raised when an invoke is called
// using a provider that is not configured.
func TestYamlUnconfiguredProvider(t *testing.T) {
cwd, err := os.Getwd()
if !assert.NoError(t, err) {
t.FailNow()
}
options := baseOptions.With(integration.ProgramTestOptions{
Dir: filepath.Join(cwd, "yaml-test-unconfigured-provider"),
ExpectRefreshChanges: true,
OrderedConfig: []integration.ConfigValue{
{
Key: "pulumi:disable-default-providers[0]",
Value: "kubernetes",
Path: true,
},
},
})
integration.ProgramTest(t, &options)
}

func TestGuestbook(t *testing.T) {
cwd, err := os.Getwd()
if !assert.NoError(t, err) {
Expand Down Expand Up @@ -417,6 +439,32 @@ func TestHelmLocal(t *testing.T) {
integration.ProgramTest(t, &options)
}

// Regression Test for https://github.com/pulumi/pulumi-kubernetes/issues/2664.
// Ensure the program runs without an error being raised when an invoke is called
// using a provider that is not configured.
func TestHelmLocalUnconfiguredProvider(t *testing.T) {
cwd, err := os.Getwd()
if !assert.NoError(t, err) {
t.FailNow()
}
options := baseOptions.With(integration.ProgramTestOptions{
Dir: filepath.Join(cwd, "helm-local-unconfigured-provider"),
OrderedConfig: []integration.ConfigValue{
{
Key: "pulumi:disable-default-providers[0]",
Value: "kubernetes",
Path: true,
},
{
Key: "path",
Value: filepath.Join(cwd, "..", "..", "testdata", "helm", "nginx"),
},
},
ExpectRefreshChanges: true,
})
integration.ProgramTest(t, &options)
}

func TestHelmApiVersions(t *testing.T) {
cwd, err := os.Getwd()
if !assert.NoError(t, err) {
Expand Down Expand Up @@ -484,6 +532,27 @@ func TestKustomize(t *testing.T) {
integration.ProgramTest(t, &options)
}

// Regression Test for https://github.com/pulumi/pulumi-kubernetes/issues/2664.
// Ensure the program runs without an error being raised when an invoke is called
// using a provider that is not configured.
func TestKustomizeUnconfiguredProvider(t *testing.T) {
cwd, err := os.Getwd()
if !assert.NoError(t, err) {
t.FailNow()
}
options := baseOptions.With(integration.ProgramTestOptions{
Dir: filepath.Join(cwd, "kustomize-unconfigured-provider"),
OrderedConfig: []integration.ConfigValue{
{
Key: "pulumi:disable-default-providers[0]",
Value: "kubernetes",
Path: true,
},
},
})
integration.ProgramTest(t, &options)
}

func TestSecrets(t *testing.T) {
cwd, err := os.Getwd()
if !assert.NoError(t, err) {
Expand Down
3 changes: 3 additions & 0 deletions tests/sdk/python/yaml-test-unconfigured-provider/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: yaml-test-unconfigured-provider
description: A program that tests YAML functionality in the Python SDK with an unconfigured provider
runtime: python
66 changes: 66 additions & 0 deletions tests/sdk/python/yaml-test-unconfigured-provider/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Copyright 2016-2023, 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.

from pulumi_kubernetes import Provider
from pulumi_kubernetes.core.v1 import Namespace
from pulumi_kubernetes.yaml import ConfigFile, ConfigGroup
from pulumi import ResourceOptions


def set_namespace(namespace):
def f(obj):
if "metadata" in obj:
obj["metadata"]["namespace"] = namespace.metadata["name"]
else:
obj["metadata"] = {"namespace": namespace.metadata["name"]}

return f


def secret_status(obj, opts):
if obj["kind"] == "Pod" and obj["apiVersion"] == "v1":
opts.additional_secret_outputs = ["apiVersion"]


# This will be unknown during the initial preview.
unknown = Provider("provider").id.apply(lambda _: True)

# This provider will be unconfigured when the passed-in configuration has an unknown value.
provider = Provider("k8s", suppress_deprecation_warnings=unknown)

ns = Namespace("unconfiguredtest", opts=ResourceOptions(provider=provider))

# An error shouldn't be raised when called using the unconfigured provider.
cf_local = ConfigFile(
"yaml-test",
"manifest.yaml",
transformations=[
set_namespace(ns),
secret_status,
],
opts=ResourceOptions(provider=provider),
)
justinvp marked this conversation as resolved.
Show resolved Hide resolved

# An error shouldn't be raised when called using the unconfigured provider.
cg = ConfigGroup(
"deployment",
files=["ns*.yaml"],
yaml=["""
apiVersion: v1
kind: Namespace
metadata:
name: utcg3
"""],
opts=ResourceOptions(provider=provider)
)
Loading
Loading