Skip to content

Commit

Permalink
Handle unknowns in Helm Release
Browse files Browse the repository at this point in the history
  • Loading branch information
EronWright committed Feb 14, 2024
1 parent 33c84f9 commit 4bae2da
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 36 deletions.
70 changes: 44 additions & 26 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
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 @@ -877,7 +887,7 @@ func (r *helmReleaseProvider) Create(ctx context.Context, req *pulumirpc.CreateR
}
}

obj := checkpointRelease(news, newRelease, fmt.Sprintf("%s.news", label))
obj := checkpointRelease(news, newRelease, fmt.Sprintf("%s.news", label), req.GetPreview())
inputsAndComputed, err := plugin.MarshalProperties(
obj, plugin.MarshalOptions{
Label: fmt.Sprintf("%s.inputsAndComputed", label),
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 @@ -970,7 +980,7 @@ func (r *helmReleaseProvider) Read(ctx context.Context, req *pulumirpc.ReadReque

// Return a new "checkpoint object".
state, err := plugin.MarshalProperties(
checkpointRelease(oldInputs, existingRelease, fmt.Sprintf("%s.olds", label)), plugin.MarshalOptions{
checkpointRelease(oldInputs, existingRelease, fmt.Sprintf("%s.olds", label), false), plugin.MarshalOptions{
Label: fmt.Sprintf("%s.state", label),
KeepUnknowns: true,
SkipNulls: true,
Expand Down Expand Up @@ -1051,7 +1061,7 @@ func (r *helmReleaseProvider) Update(ctx context.Context, req *pulumirpc.UpdateR
}
}

checkpointed := checkpointRelease(newResInputs, newRelease, fmt.Sprintf("%s.news", label))
checkpointed := checkpointRelease(newResInputs, newRelease, fmt.Sprintf("%s.news", label), req.GetPreview())
inputsAndComputed, err := plugin.MarshalProperties(
checkpointed, plugin.MarshalOptions{
Label: fmt.Sprintf("%s.inputsAndComputed", label),
Expand Down Expand Up @@ -1117,15 +1127,23 @@ func (r *helmReleaseProvider) Delete(ctx context.Context, req *pulumirpc.DeleteR
return &pbempty.Empty{}, nil
}

func checkpointRelease(inputs resource.PropertyMap, outputs *Release, label string) resource.PropertyMap {
func checkpointRelease(inputs resource.PropertyMap, outputs *Release, label string, isPreview bool) resource.PropertyMap {
logger.V(9).Infof("[%s] Checkpointing outputs: %#v", label, outputs)
logger.V(9).Infof("[%s] Checkpointing inputs: %#v", label, inputs)
object := resource.NewPropertyMap(outputs)
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)

if isPreview {
// Mark the pure outputs as computed.
object["resourceNames"] = resource.MakeComputed(resource.NewArrayProperty([]resource.PropertyValue{}))
object["status"] = resource.MakeComputed(resource.NewObjectProperty(resource.PropertyMap{}))
}

return object
}

Expand Down
47 changes: 37 additions & 10 deletions provider/pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1525,6 +1525,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 @@ -1565,10 +1568,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 @@ -1740,8 +1739,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 @@ -2209,6 +2207,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 @@ -2249,9 +2251,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 @@ -2761,6 +2760,17 @@ func normalizeInputs(uns *unstructured.Unstructured) (*unstructured.Unstructured
return uns, nil
}

func combineMapReplv(replvs ...func(resource.PropertyValue) (interface{}, bool)) func(resource.PropertyValue) (interface{}, bool) {
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 @@ -2769,6 +2779,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 @@ -3000,7 +3018,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 {
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++ {
if i < len(outs) {
outs[i] = annotateComputedValue(outs[i], ins[i])
}
}
}

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

0 comments on commit 4bae2da

Please sign in to comment.