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

Handle unknowns in Helm Release #2822

Merged
merged 8 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
- Use output properties for await logic (https://github.com/pulumi/pulumi-kubernetes/pull/2790)
- Support for metadata.generateName (CSA) (https://github.com/pulumi/pulumi-kubernetes/pull/2808)
- Fix unmarshalling of Helm values yaml file (https://github.com/pulumi/pulumi-kubernetes/issues/2815)

- Handle unknowns in Helm Release resource (https://github.com/pulumi/pulumi-kubernetes/pull/2822)

## 4.7.1 (January 17, 2024)

- Fix deployment await logic for accurate rollout detection
Expand Down
41 changes: 41 additions & 0 deletions provider/pkg/provider/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

jsonpatch "github.com/evanphx/json-patch"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -191,6 +192,46 @@ func TestPatchToDiff(t *testing.T) {
"data.property1": UR,
},
},
{
name: `Changing computed object values results in correct diff`,
group: "core", version: "v1", kind: "Pod",
old: object{"spec": object{"containers": list{object{"name": "nginx", "image": "nginx"}}}},
new: object{"spec": object{"containers": list{object{"name": "nginx", "image": nil}}}},
inputs: object{"spec": object{"containers": list{object{"name": "nginx", "image": resource.Computed{}}}}},
expected: expected{
"spec.containers[0].image": UR,
},
},
{
name: `Adding computed object values results in correct diff`,
group: "core", version: "v1", kind: "Pod",
old: object{"spec": object{"containers": list{object{"name": "nginx"}}}},
new: object{"spec": object{"containers": list{object{"name": "nginx", "image": nil}}}},
inputs: object{"spec": object{"containers": list{object{"name": "nginx", "image": resource.Computed{}}}}},
expected: expected{
"spec.containers[0].image": AR,
},
},
{
name: `Adding computed array values results in correct diff`,
group: "core", version: "v1", kind: "Pod",
old: object{"spec": object{"containers": list{}}},
new: object{"spec": object{"containers": list{nil}}},
inputs: object{"spec": object{"containers": list{resource.Computed{}}}},
expected: expected{
"spec.containers[0]": A,
},
},
{
name: `Changing computed array values results in correct diff`,
group: "core", version: "v1", kind: "Pod",
old: object{"spec": object{"containers": list{object{"name": "nginx"}}}},
new: object{"spec": object{"containers": list{nil}}},
inputs: object{"spec": object{"containers": list{resource.Computed{}}}},
expected: expected{
"spec.containers[0]": U,
},
},
}

for _, tt := range tests {
Expand Down
58 changes: 36 additions & 22 deletions provider/pkg/provider/helm_release.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,14 +276,18 @@ func (r *helmReleaseProvider) getActionConfig(namespace string) (*action.Configu
return conf, nil
}

var (
// mapReplExtractValues extracts pure values from the property map.
mapReplExtractValues = combineMapReplv(mapReplStripSecrets, mapReplStripComputed)
)

func decodeRelease(pm resource.PropertyMap, label string) (*Release, error) {
var release Release
values := map[string]any{}
stripped := pm.MapRepl(nil, mapReplStripSecrets)
stripped := pm.MapRepl(nil, mapReplExtractValues)
logger.V(9).Infof("[%s] Decoding release: %#v", label, stripped)

if pm.HasValue("valueYamlFiles") {
v := stripped["valueYamlFiles"]
if v, ok := stripped["valueYamlFiles"]; ok {
switch reflect.TypeOf(v).Kind() {
case reflect.Slice, reflect.Array:
s := reflect.ValueOf(v)
Expand Down Expand Up @@ -344,19 +348,19 @@ func (r *helmReleaseProvider) Check(ctx context.Context, req *pulumirpc.CheckReq
return nil, pkgerrors.Wrapf(err, "check failed because malformed resource inputs: %+v", err)
}

if len(olds.Mappable()) > 0 {
if len(olds) > 0 {
adoptOldNameIfUnnamed(news, olds)
}
assignNameIfAutonameable(news, urn)
r.setDefaults(news)

if !news.ContainsUnknowns() {
logger.V(9).Infof("Decoding new release.")
new, err := decodeRelease(news, fmt.Sprintf("%s.news", label))
if err != nil {
return nil, err
}
logger.V(9).Infof("Decoding new release.")
new, err := decodeRelease(news, fmt.Sprintf("%s.news", label))
if err != nil {
return nil, err
}

if !news.ContainsUnknowns() {
logger.V(9).Infof("Loading Helm chart.")
chart, err := r.helmLoad(ctx, urn, new)
if err != nil {
Expand All @@ -370,11 +374,11 @@ func (r *helmReleaseProvider) Check(ctx context.Context, req *pulumirpc.CheckReq
// with this we may determine whether the Helm release needs to be upgraded.
new.Version = chart.Metadata.Version
}

logger.V(9).Infof("New: %+v", new)
news = resource.NewPropertyMap(new)
}

logger.V(9).Infof("New: %+v", new)
news = resource.NewPropertyMap(new)

// remove deprecated inputs
delete(news, "resourceNames")

Expand All @@ -387,7 +391,8 @@ func (r *helmReleaseProvider) Check(ctx context.Context, req *pulumirpc.CheckReq
if err != nil {
return nil, pkgerrors.Wrapf(err, "check failed because malformed resource inputs: %+v", err)
}
// ensure we don't leak secrets into state.
// ensure we don't leak secrets into state, and preserve the computedness of inputs.
annotateComputed(news, newInputs)
annotateSecrets(news, newInputs)

autonamedInputs, err := plugin.MarshalProperties(news, plugin.MarshalOptions{
Expand Down Expand Up @@ -743,17 +748,18 @@ func (r *helmReleaseProvider) Diff(ctx context.Context, req *pulumirpc.DiffReque
logger.V(9).Infof("Diff: New release: %#v", newRelease)

// Generate a patch to apply the new inputs to the old state, including deletions.
oldInputsJSON, err := json.Marshal(oldInputs.MapRepl(nil, mapReplStripSecrets))
// Note that computed values are seen as nulls and
EronWright marked this conversation as resolved.
Show resolved Hide resolved
oldInputsJSON, err := json.Marshal(oldInputs.MapRepl(nil, mapReplExtractValues))
if err != nil {
return nil, pkgerrors.Wrapf(err, "internal error: json.Marshal(oldInputsJson)")
}
logger.V(9).Infof("oldInputsJSON: %s", string(oldInputsJSON))
newInputsJSON, err := json.Marshal(news.MapRepl(nil, mapReplStripSecrets))
newInputsJSON, err := json.Marshal(news.MapRepl(nil, mapReplExtractValues))
if err != nil {
return nil, pkgerrors.Wrapf(err, "internal error: json.Marshal(oldInputsJson)")
}
logger.V(9).Infof("newInputsJSON: %s", string(newInputsJSON))
oldStateJSON, err := json.Marshal(olds.MapRepl(nil, mapReplStripSecrets))
oldStateJSON, err := json.Marshal(olds.MapRepl(nil, mapReplExtractValues))
if err != nil {
return nil, pkgerrors.Wrapf(err, "internal error: json.Marshal(oldStateJson)")
}
Expand All @@ -768,7 +774,7 @@ func (r *helmReleaseProvider) Diff(ctx context.Context, req *pulumirpc.DiffReque
return nil, pkgerrors.Wrapf(
err, "Failed to check for changes in Helm release %s/%s because of an error serializing "+
"the JSON patch describing resource changes",
newRelease.Namespace, newRelease.Name)
oldRelease.Namespace, oldRelease.Name)
}

// Pack up PB, ship response back.
Expand All @@ -788,12 +794,16 @@ func (r *helmReleaseProvider) Diff(ctx context.Context, req *pulumirpc.DiffReque
logger.V(9).Infof("news: %+v", news.Mappable())
logger.V(9).Infof("oldInputs: %+v", oldInputs.Mappable())

strip := func(pm resource.PropertyMap) map[string]interface{} {
// strip the secretness but retain computedness (as is understood by convertPatchToDiff)
return pm.MapRepl(nil, mapReplStripSecrets)
}
forceNewFields := []string{".name", ".namespace"}
if detailedDiff, err = convertPatchToDiff(patchObj, olds.Mappable(), news.Mappable(), oldInputs.Mappable(), forceNewFields...); err != nil {
if detailedDiff, err = convertPatchToDiff(patchObj, strip(olds), strip(news), strip(oldInputs), forceNewFields...); err != nil {
return nil, pkgerrors.Wrapf(
err, "Failed to check for changes in helm release %s/%s because of an error "+
"converting JSON patch describing resource changes to a diff",
newRelease.Namespace, newRelease.Name)
oldRelease.Namespace, oldRelease.Name)
}

for k, v := range detailedDiff {
Expand Down Expand Up @@ -926,7 +936,7 @@ func (r *helmReleaseProvider) Read(ctx context.Context, req *pulumirpc.ReadReque
logger.V(9).Infof("%s decoded release: %#v", label, existingRelease)

var namespace, name string
if len(oldState.Mappable()) == 0 {
if len(oldState) == 0 {
namespace, name = parseFqName(req.GetId())
logger.V(9).Infof("%s Starting import for %s/%s", label, namespace, name)
} else {
Expand Down Expand Up @@ -1124,8 +1134,12 @@ func checkpointRelease(inputs resource.PropertyMap, outputs *Release, label stri
object["__inputs"] = resource.MakeSecret(resource.NewObjectProperty(inputs))

// Make sure parts of the inputs which are marked as secrets in the inputs are retained as
// secrets in the outputs.
// secrets in the outputs. Likewise for computed values.
annotateComputed(object, inputs)
annotateSecrets(object, inputs)

// note that status and resourceNames are left undefined in previews.

return object
}

Expand Down
58 changes: 41 additions & 17 deletions provider/pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,9 @@ func (k *kubeProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*p
//

urn := resource.URN(req.GetUrn())
if isHelmRelease(urn) {
return k.helmReleaseProvider.Diff(ctx, req)
}

label := fmt.Sprintf("%s.Diff(%s)", k.label(), urn)
logger.V(9).Infof("%s executing", label)
Expand Down Expand Up @@ -1559,10 +1562,7 @@ func (k *kubeProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*p
newInputs := propMapToUnstructured(newResInputs)

oldInputs, oldLive := parseCheckpointObject(oldState)
if !isHelmRelease(urn) {
// "isHelmRelease" is due to https://github.com/pulumi/pulumi-kubernetes/issues/2679
contract.Assertf(oldLive.GetName() != "", "expected live object name to be nonempty: %v", oldLive)
}
contract.Assertf(oldLive.GetName() != "", "expected live object name to be nonempty: %v", oldLive)

oldInputs, err = normalizeInputs(oldInputs)
if err != nil {
Expand All @@ -1576,10 +1576,6 @@ func (k *kubeProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*p

gvk := k.gvkFromUnstructured(newInputs)

if isHelmRelease(urn) && !hasComputedValue(newInputs) {
return k.helmReleaseProvider.Diff(ctx, req)
}

namespacedKind, err := clients.IsNamespacedKind(gvk, k.clientSet)
if err != nil {
if clients.IsNoNamespaceInfoErr(err) {
Expand Down Expand Up @@ -1759,8 +1755,7 @@ func (k *kubeProvider) Create(
// resource. This is important both for `Diff` and for `Update`. See comments in those methods for details.
//
urn := resource.URN(req.GetUrn())

if isHelmRelease(urn) && !req.GetPreview() {
if isHelmRelease(urn) {
return k.helmReleaseProvider.Create(ctx, req)
}

Expand Down Expand Up @@ -2235,6 +2230,10 @@ func (k *kubeProvider) Update(
//

urn := resource.URN(req.GetUrn())
if isHelmRelease(urn) {
return k.helmReleaseProvider.Update(ctx, req)
}

label := fmt.Sprintf("%s.Update(%s)", k.label(), urn)
logger.V(9).Infof("%s executing", label)

Expand Down Expand Up @@ -2275,9 +2274,6 @@ func (k *kubeProvider) Update(
return &pulumirpc.UpdateResponse{Properties: req.News}, nil
}

if isHelmRelease(urn) {
return k.helmReleaseProvider.Update(ctx, req)
}
// Ignore old state; we'll get it from Kubernetes later.
oldInputs, oldLive := parseCheckpointObject(oldState)

Expand Down Expand Up @@ -2790,6 +2786,17 @@ func normalizeInputs(uns *unstructured.Unstructured) (*unstructured.Unstructured
return uns, nil
}

func combineMapReplv(replvs ...func(resource.PropertyValue) (interface{}, bool)) func(resource.PropertyValue) (interface{}, bool) {
EronWright marked this conversation as resolved.
Show resolved Hide resolved
return func(v resource.PropertyValue) (interface{}, bool) {
for _, replv := range replvs {
if r, ok := replv(v); ok {
return r, true
}
}
return "", false
}
}

func mapReplStripSecrets(v resource.PropertyValue) (any, bool) {
if v.IsSecret() {
return v.SecretValue().Element.MapRepl(nil, mapReplStripSecrets), true
Expand All @@ -2798,6 +2805,14 @@ func mapReplStripSecrets(v resource.PropertyValue) (any, bool) {
return nil, false
}

func mapReplStripComputed(v resource.PropertyValue) (any, bool) {
if v.IsComputed() {
return nil, true
}

return nil, false
}

// mapReplUnderscoreToDash is needed to work around cases where SDKs don't allow dashes in variable names, and so the
// parameter is renamed with an underscore during schema generation. This function normalizes those keys to the format
// expected by the cluster.
Expand Down Expand Up @@ -3015,9 +3030,9 @@ type patchConverter struct {
func (pc *patchConverter) addPatchValueToDiff(
path []any, v, old, newInput, oldInput any, inArray bool,
) error {
contract.Assertf(v != nil || old != nil || oldInput != nil,
"path: %+v | v: %+v | old: %+v | oldInput: %+v",
path, v, old, oldInput)
contract.Assertf(v != nil || old != nil || oldInput != nil || newInput != nil,
"path: %+v | v: %+v | old: %+v | oldInput: %+v | newInput: %+v",
path, v, old, oldInput, newInput)

// If there is no new input, then the only possible diff here is a delete. All other diffs must be diffs between
// old and new properties that are populated by the server. If there is also no old input, then there is no diff
Expand All @@ -3029,7 +3044,16 @@ func (pc *patchConverter) addPatchValueToDiff(
var diffKind pulumirpc.PropertyDiff_Kind
inputDiff := false
if v == nil {
diffKind, inputDiff = pulumirpc.PropertyDiff_DELETE, true
// computed values are rendered as null in the patch; handle this special case.
if _, ok := newInput.(resource.Computed); ok {
if old == nil {
diffKind = pulumirpc.PropertyDiff_ADD
} else {
diffKind = pulumirpc.PropertyDiff_UPDATE
}
} else {
diffKind, inputDiff = pulumirpc.PropertyDiff_DELETE, true
}
} else if old == nil {
diffKind = pulumirpc.PropertyDiff_ADD
} else {
Expand Down
36 changes: 36 additions & 0 deletions provider/pkg/provider/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,42 @@ func hasComputedValue(obj *unstructured.Unstructured) bool {
return false
}

func annotateComputed(outs, ins resource.PropertyMap) {
if outs == nil || ins == nil {
return
}
for key, inValue := range ins {
outValue, has := outs[key]
if !has {
continue
}
outs[key] = annotateComputedValue(outValue, inValue)
}
}

func annotateComputedValue(outValue, inValue resource.PropertyValue) resource.PropertyValue {
EronWright marked this conversation as resolved.
Show resolved Hide resolved
if inValue.IsSecret() {
inValue = inValue.SecretValue().Element
}
if !outValue.IsComputed() && inValue.IsComputed() {
return resource.MakeComputed(inValue.Input().Element)
}
if outValue.IsObject() && inValue.IsObject() {
annotateComputed(outValue.ObjectValue(), inValue.ObjectValue())
} else if outValue.IsArray() && inValue.IsArray() {
annotateComputedArray(outValue.ArrayValue(), inValue.ArrayValue())
}
return outValue
}

func annotateComputedArray(outs, ins []resource.PropertyValue) {
for i := 0; i < len(ins); i++ {
EronWright marked this conversation as resolved.
Show resolved Hide resolved
if i < len(outs) {
outs[i] = annotateComputedValue(outs[i], ins[i])
}
}
}

// --------------------------------------------------------------------------
// Names and namespaces.
// --------------------------------------------------------------------------
Expand Down
Loading
Loading