Skip to content

Commit

Permalink
[internal] Add function to prune map keys (#2436)
Browse files Browse the repository at this point in the history
Add a "pruneMap" function and supporting "pruneSlice" function to prune map keys that are not present in a target map.

Kubernetes sets a significant amount of additional state on cluster resources after they are created, which complicates diffing with the subset of properties that are set by a user program. The pruneMap function can be used to preprocess live resource state before diffing against program inputs.
  • Loading branch information
lblackstone committed Jun 2, 2023
1 parent df71ef2 commit 14916d3
Show file tree
Hide file tree
Showing 2 changed files with 965 additions and 0 deletions.
86 changes: 86 additions & 0 deletions provider/pkg/provider/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ package provider
import (
"encoding/json"
"fmt"
"reflect"
"strings"

"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/deepcopy"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/tools/clientcmd"
Expand Down Expand Up @@ -133,3 +135,87 @@ func getActiveClusterFromConfig(config *clientapi.Config, overrides resource.Pro

return activeCluster
}

// pruneMap builds a pruned map by recursively copying elements from the source map that have a matching key in the
// target map. This is useful as a preprocessing step for live resource state before comparing it to program inputs.
func pruneMap(source, target map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})

for key, value := range source {
valueT := reflect.TypeOf(value)

if targetValue, ok := target[key]; ok {
targetValueT := reflect.TypeOf(targetValue)

if valueT == nil || targetValueT == nil || valueT != targetValueT {
result[key] = value
continue
}

switch valueT.Kind() {
case reflect.Map:
nestedResult := pruneMap(value.(map[string]interface{}), targetValue.(map[string]interface{}))
if len(nestedResult) > 0 {
result[key] = nestedResult
}
case reflect.Slice:
nestedResult := pruneSlice(value.([]interface{}), targetValue.([]interface{}))
if len(nestedResult) > 0 {
result[key] = nestedResult
}
default:
result[key] = value
}
}
}

return result
}

// pruneSlice builds a pruned slice by copying elements from the source slice that have a matching element in the
// target slice.
func pruneSlice(source, target []interface{}) []interface{} {
result := make([]interface{}, 0, len(target))

// If either slice is empty, return an empty slice.
if len(source) == 0 || len(target) == 0 {
return result
}

valueT := reflect.TypeOf(source[0])
targetValueT := reflect.TypeOf(target[0])

// If slices are of different types, return a copy of the source.
if valueT != targetValueT {
return deepcopy.Copy(source).([]interface{})
}

for i, targetValue := range target {
if i+1 > len(source) {
break
}
value := source[i]

if value == nil || targetValue == nil {
result = append(result, value)
continue
}

switch valueT.Kind() {
case reflect.Map:
nestedResult := pruneMap(value.(map[string]interface{}), targetValue.(map[string]interface{}))
if len(nestedResult) > 0 {
result = append(result, nestedResult)
}
case reflect.Slice:
nestedResult := pruneSlice(value.([]interface{}), targetValue.([]interface{}))
if len(nestedResult) > 0 {
result = append(result, nestedResult)
}
default:
result = append(result, value)
}
}

return result
}
Loading

0 comments on commit 14916d3

Please sign in to comment.