Skip to content

Instantly share code, notes, and snippets.

@Kazy
Created March 23, 2026 20:35
Show Gist options
  • Select an option

  • Save Kazy/1f8d5867e31f043621b21add95e48927 to your computer and use it in GitHub Desktop.

Select an option

Save Kazy/1f8d5867e31f043621b21add95e48927 to your computer and use it in GitHub Desktop.
Pulumi TF Bridge CloudRun fix patch
diff --git a/pkg/tfbridge/schema.go b/pkg/tfbridge/schema.go
index b635cbf610..4b2c9ea6e0 100644
--- a/pkg/tfbridge/schema.go
+++ b/pkg/tfbridge/schema.go
@@ -463,10 +463,18 @@
etfs, eps := elemSchemas(tfs, ps)
+ newArr := v.ArrayValue()
+
+ // For TypeSets, match old elements to new elements by content rather than
+ // position, since providers may return set elements in arbitrary order.
+ oldToNewMap := matchOldToNewByContent(oldArr, newArr, tfs)
+
var arr []interface{}
- for i, elem := range v.ArrayValue() {
+ for i, elem := range newArr {
var oldElem resource.PropertyValue
- if i < len(oldArr) {
+ if oldIdx, ok := oldToNewMap[i]; ok {
+ oldElem = oldArr[oldIdx]
+ } else if tfs.Type() != shim.TypeSet && i < len(oldArr) {
oldElem = oldArr[i]
}
elemName := fmt.Sprintf("%v[%v]", name, i)
@@ -1608,6 +1616,44 @@
return floatVal, nil
}
+// matchOldToNewByContent builds a mapping from new array indices to old array indices
+// using content-based matching. For TypeSet arrays, this prevents incorrect old/new
+// pairing when the provider returns elements in a different order.
+// Returns a map of newIndex -> oldIndex.
+func matchOldToNewByContent(
+ oldArr, newArr []resource.PropertyValue, tfs shim.Schema,
+) map[int]int {
+ if tfs == nil || tfs.Type() != shim.TypeSet || len(oldArr) == 0 || len(newArr) == 0 {
+ return nil
+ }
+
+ usedOld := make(map[int]bool)
+ newToOld := make(map[int]int)
+
+ for newIdx, newElem := range newArr {
+ bestOldIdx := -1
+ bestScore := 0
+
+ for oldIdx, oldElem := range oldArr {
+ if usedOld[oldIdx] {
+ continue
+ }
+ score := setElementMatchScore(oldElem, newElem)
+ if score > bestScore {
+ bestScore = score
+ bestOldIdx = oldIdx
+ }
+ }
+
+ if bestOldIdx >= 0 {
+ newToOld[newIdx] = bestOldIdx
+ usedOld[bestOldIdx] = true
+ }
+ }
+
+ return newToOld
+}
+
func min(a int, b int) int {
if a < b {
return a
@@ -1615,6 +1661,81 @@
return b
}
+// extractInputsForSet matches old and new set elements by content rather than by
+// position, then delegates to extractInputs for each matched pair. This prevents
+// field corruption when the cloud provider returns set elements in a different order
+// than the user specified.
+func extractInputsForSet(
+ oldArray, newArray []resource.PropertyValue,
+ etfs shim.Schema, eps *SchemaInfo,
+) (resource.PropertyValue, bool) {
+ possibleDefault := true
+
+ // Build old-to-new mapping using content-based matching.
+ usedNew := make(map[int]bool)
+ oldToNew := make(map[int]int)
+
+ for oldIdx, oldElem := range oldArray {
+ bestNewIdx := -1
+ bestScore := 0
+
+ for newIdx, newElem := range newArray {
+ if usedNew[newIdx] {
+ continue
+ }
+ score := setElementMatchScore(oldElem, newElem)
+ if score > bestScore {
+ bestScore = score
+ bestNewIdx = newIdx
+ }
+ }
+
+ if bestNewIdx >= 0 {
+ oldToNew[oldIdx] = bestNewIdx
+ usedNew[bestNewIdx] = true
+ }
+ }
+
+ result := make([]resource.PropertyValue, 0, min(len(oldArray), len(newArray)))
+ for oldIdx := range oldArray {
+ newIdx, ok := oldToNew[oldIdx]
+ if !ok {
+ possibleDefault = false
+ continue
+ }
+
+ extracted, defaultElem := extractInputs(oldArray[oldIdx], newArray[newIdx], etfs, eps)
+ result = append(result, extracted)
+ if !defaultElem {
+ possibleDefault = false
+ }
+ }
+
+ return resource.NewArrayProperty(result), possibleDefault
+}
+
+// setElementMatchScore returns a score indicating how well two property values match.
+// Higher scores indicate a better match. For objects, this counts matching key-value pairs.
+func setElementMatchScore(a, b resource.PropertyValue) int {
+ if a.IsObject() && b.IsObject() {
+ score := 0
+ aMap, bMap := a.ObjectValue(), b.ObjectValue()
+ for key, aVal := range aMap {
+ if reservedkeys.IsBridgeReservedKey(string(key)) {
+ continue
+ }
+ if bVal, ok := bMap[key]; ok && aVal.DeepEquals(bVal) {
+ score++
+ }
+ }
+ return score
+ }
+ if a.DeepEquals(b) {
+ return 1
+ }
+ return 0
+}
+
func extractInputs(
oldInput, newState resource.PropertyValue, tfs shim.Schema, ps *SchemaInfo,
) (resource.PropertyValue, bool) {
@@ -1628,6 +1749,11 @@
etfs, eps := elemSchemas(tfs, ps)
oldArray, newArray := oldInput.ArrayValue(), newState.ArrayValue()
+
+ if tfs != nil && tfs.Type() == shim.TypeSet {
+ return extractInputsForSet(oldArray, newArray, etfs, eps)
+ }
+
for i := range oldArray {
if i >= len(newArray) {
possibleDefault = false
diff --git a/pkg/tfbridge/schema_test.go b/pkg/tfbridge/schema_test.go
index b8a84398e2..8710062c32 100644
--- a/pkg/tfbridge/schema_test.go
+++ b/pkg/tfbridge/schema_test.go
@@ -2299,6 +2299,403 @@
}, actual)
}
+// TestRefreshExtractInputsTypeSetReorderPreservesFields reproduces the bug
+// reported in https://github.com/pulumi/pulumi-terraform-bridge/issues/3383.
+// When a TypeSet is reordered by the cloud provider, extractInputs must match
+// elements by content rather than by position to avoid dropping fields.
+func TestRefreshExtractInputsTypeSetReorderPreservesFields(t *testing.T) {
+ t.Parallel()
+
+ envVarSchema := func() shim.SchemaMap {
+ return schema.SchemaMap{
+ "envs": (&schema.Schema{
+ Type: shim.TypeSet,
+ Optional: true,
+ Elem: (&schema.Resource{
+ Schema: schema.SchemaMap{
+ "name": (&schema.Schema{Type: shim.TypeString, Required: true}).Shim(),
+ "value": (&schema.Schema{Type: shim.TypeString, Optional: true}).Shim(),
+ "value_source": (&schema.Schema{
+ Type: shim.TypeList,
+ Optional: true,
+ MaxItems: 1,
+ Elem: (&schema.Resource{
+ Schema: schema.SchemaMap{
+ "secret_key_ref": (&schema.Schema{
+ Type: shim.TypeList,
+ Optional: true,
+ MaxItems: 1,
+ Elem: (&schema.Resource{
+ Schema: schema.SchemaMap{
+ "secret": (&schema.Schema{Type: shim.TypeString, Required: true}).Shim(),
+ "version": (&schema.Schema{Type: shim.TypeString, Optional: true}).Shim(),
+ },
+ }).Shim(),
+ }).Shim(),
+ },
+ }).Shim(),
+ }).Shim(),
+ },
+ }).Shim(),
+ }).Shim(),
+ }
+ }
+
+ // User-specified inputs: ZOO_MODE first, then APP_SECRET with valueSource.
+ oldInputs := resource.PropertyMap{
+ "envs": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("ZOO_MODE"),
+ "value": resource.NewStringProperty("production"),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("APP_SECRET"),
+ "valueSource": resource.NewObjectProperty(resource.PropertyMap{
+ "secretKeyRef": resource.NewObjectProperty(resource.PropertyMap{
+ "secret": resource.NewStringProperty("my-secret"),
+ "version": resource.NewStringProperty("latest"),
+ }),
+ }),
+ }),
+ }),
+ }
+
+ // GCP returns alphabetically: APP_SECRET first, ZOO_MODE second.
+ // APP_SECRET gets value:"" from TF Default, plus its valueSource.
+ newOutputs := resource.PropertyMap{
+ "envs": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("APP_SECRET"),
+ "value": resource.NewStringProperty(""),
+ "valueSource": resource.NewObjectProperty(resource.PropertyMap{
+ "secretKeyRef": resource.NewObjectProperty(resource.PropertyMap{
+ "secret": resource.NewStringProperty("my-secret"),
+ "version": resource.NewStringProperty("latest"),
+ }),
+ }),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("ZOO_MODE"),
+ "value": resource.NewStringProperty("production"),
+ }),
+ }),
+ }
+
+ result, err := ExtractInputsFromOutputs(oldInputs, newOutputs, envVarSchema(), nil, true)
+ require.NoError(t, err)
+
+ envs := result["envs"].ArrayValue()
+ require.Len(t, envs, 2)
+
+ // Find the APP_SECRET entry in the result.
+ var appSecret resource.PropertyMap
+ for _, env := range envs {
+ obj := env.ObjectValue()
+ if obj["name"].StringValue() == "APP_SECRET" {
+ appSecret = obj
+ break
+ }
+ }
+
+ require.NotNil(t, appSecret, "APP_SECRET entry should be present")
+
+ // The critical assertion: valueSource must NOT be dropped.
+ vs, hasValueSource := appSecret["valueSource"]
+ assert.True(t, hasValueSource, "valueSource must be preserved after refresh with reordered TypeSet")
+ if hasValueSource {
+ secretRef := vs.ObjectValue()["secretKeyRef"].ObjectValue()
+ assert.Equal(t, "my-secret", secretRef["secret"].StringValue())
+ assert.Equal(t, "latest", secretRef["version"].StringValue())
+ }
+
+ // ZOO_MODE should retain its value.
+ var zooMode resource.PropertyMap
+ for _, env := range envs {
+ obj := env.ObjectValue()
+ if obj["name"].StringValue() == "ZOO_MODE" {
+ zooMode = obj
+ break
+ }
+ }
+ require.NotNil(t, zooMode, "ZOO_MODE entry should be present")
+ assert.Equal(t, "production", zooMode["value"].StringValue())
+}
+
+func TestRefreshExtractInputsTypeSetElementRemoved(t *testing.T) {
+ t.Parallel()
+
+ // When the provider returns fewer set elements than old inputs, the unmatched
+ // old element should be dropped and the matched ones preserved correctly.
+ tagSchema := func() shim.SchemaMap {
+ return schema.SchemaMap{
+ "tags": (&schema.Schema{
+ Type: shim.TypeSet,
+ Optional: true,
+ Elem: (&schema.Resource{
+ Schema: schema.SchemaMap{
+ "key": (&schema.Schema{Type: shim.TypeString, Required: true}).Shim(),
+ "value": (&schema.Schema{Type: shim.TypeString, Optional: true}).Shim(),
+ },
+ }).Shim(),
+ }).Shim(),
+ }
+ }
+
+ oldInputs := resource.PropertyMap{
+ "tags": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "key": resource.NewStringProperty("env"),
+ "value": resource.NewStringProperty("prod"),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "key": resource.NewStringProperty("team"),
+ "value": resource.NewStringProperty("infra"),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "key": resource.NewStringProperty("temp"),
+ "value": resource.NewStringProperty("delete-me"),
+ }),
+ }),
+ }
+
+ // Provider returns only 2 elements (temp was removed), in different order.
+ newOutputs := resource.PropertyMap{
+ "tags": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "key": resource.NewStringProperty("team"),
+ "value": resource.NewStringProperty("infra"),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "key": resource.NewStringProperty("env"),
+ "value": resource.NewStringProperty("prod"),
+ }),
+ }),
+ }
+
+ result, err := ExtractInputsFromOutputs(oldInputs, newOutputs, tagSchema(), nil, true)
+ require.NoError(t, err)
+
+ tags := result["tags"].ArrayValue()
+ require.Len(t, tags, 2)
+
+ keys := map[string]string{}
+ for _, tag := range tags {
+ obj := tag.ObjectValue()
+ keys[obj["key"].StringValue()] = obj["value"].StringValue()
+ }
+ assert.Equal(t, "prod", keys["env"])
+ assert.Equal(t, "infra", keys["team"])
+ _, hasTemp := keys["temp"]
+ assert.False(t, hasTemp, "removed element should not be present")
+}
+
+func TestRefreshExtractInputsTypeSetElementAdded(t *testing.T) {
+ t.Parallel()
+
+ // When the provider returns more set elements than old inputs, extra elements
+ // are not in the user's inputs and should not appear in extracted inputs.
+ tagSchema := func() shim.SchemaMap {
+ return schema.SchemaMap{
+ "tags": (&schema.Schema{
+ Type: shim.TypeSet,
+ Optional: true,
+ Elem: (&schema.Resource{
+ Schema: schema.SchemaMap{
+ "key": (&schema.Schema{Type: shim.TypeString, Required: true}).Shim(),
+ "value": (&schema.Schema{Type: shim.TypeString, Optional: true}).Shim(),
+ },
+ }).Shim(),
+ }).Shim(),
+ }
+ }
+
+ oldInputs := resource.PropertyMap{
+ "tags": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "key": resource.NewStringProperty("env"),
+ "value": resource.NewStringProperty("prod"),
+ }),
+ }),
+ }
+
+ // Provider returns 2 elements — one is new (added server-side).
+ newOutputs := resource.PropertyMap{
+ "tags": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "key": resource.NewStringProperty("managed-by"),
+ "value": resource.NewStringProperty("gcp"),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "key": resource.NewStringProperty("env"),
+ "value": resource.NewStringProperty("prod"),
+ }),
+ }),
+ }
+
+ result, err := ExtractInputsFromOutputs(oldInputs, newOutputs, tagSchema(), nil, true)
+ require.NoError(t, err)
+
+ tags := result["tags"].ArrayValue()
+ // Only the user's original element should survive (min of old, new lengths
+ // with matched elements).
+ require.Len(t, tags, 1)
+ assert.Equal(t, "env", tags[0].ObjectValue()["key"].StringValue())
+}
+
+func TestRefreshExtractInputsTypeSetIdenticalElements(t *testing.T) {
+ t.Parallel()
+
+ // When all set elements are identical, any matching is valid and should not panic.
+ tagSchema := func() shim.SchemaMap {
+ return schema.SchemaMap{
+ "tags": (&schema.Schema{
+ Type: shim.TypeSet,
+ Optional: true,
+ Elem: (&schema.Resource{
+ Schema: schema.SchemaMap{
+ "key": (&schema.Schema{Type: shim.TypeString, Required: true}).Shim(),
+ "value": (&schema.Schema{Type: shim.TypeString, Optional: true}).Shim(),
+ },
+ }).Shim(),
+ }).Shim(),
+ }
+ }
+
+ elem := resource.NewObjectProperty(resource.PropertyMap{
+ "key": resource.NewStringProperty("env"),
+ "value": resource.NewStringProperty("prod"),
+ })
+
+ oldInputs := resource.PropertyMap{
+ "tags": resource.NewArrayProperty([]resource.PropertyValue{elem, elem}),
+ }
+ newOutputs := resource.PropertyMap{
+ "tags": resource.NewArrayProperty([]resource.PropertyValue{elem, elem}),
+ }
+
+ result, err := ExtractInputsFromOutputs(oldInputs, newOutputs, tagSchema(), nil, true)
+ require.NoError(t, err)
+ assert.Len(t, result["tags"].ArrayValue(), 2)
+}
+
+func TestRefreshExtractInputsTypeListNotAffected(t *testing.T) {
+ t.Parallel()
+
+ // TypeList should still use positional matching (unchanged behaviour).
+ listSchema := func() shim.SchemaMap {
+ return schema.SchemaMap{
+ "items": (&schema.Schema{
+ Type: shim.TypeList,
+ Optional: true,
+ Elem: (&schema.Resource{
+ Schema: schema.SchemaMap{
+ "name": (&schema.Schema{Type: shim.TypeString, Required: true}).Shim(),
+ "value": (&schema.Schema{Type: shim.TypeString, Optional: true}).Shim(),
+ },
+ }).Shim(),
+ }).Shim(),
+ }
+ }
+
+ oldInputs := resource.PropertyMap{
+ "items": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("first"),
+ "value": resource.NewStringProperty("a"),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("second"),
+ "value": resource.NewStringProperty("b"),
+ }),
+ }),
+ }
+
+ // Provider returns in reverse order — for TypeList, positional matching applies,
+ // so old[0] merges with new[0] and old[1] merges with new[1].
+ newOutputs := resource.PropertyMap{
+ "items": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("second"),
+ "value": resource.NewStringProperty("b"),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("first"),
+ "value": resource.NewStringProperty("a"),
+ }),
+ }),
+ }
+
+ result, err := ExtractInputsFromOutputs(oldInputs, newOutputs, listSchema(), nil, true)
+ require.NoError(t, err)
+
+ items := result["items"].ArrayValue()
+ require.Len(t, items, 2)
+ // Positional: old[0] paired with new[0], so result[0] gets new[0]'s values.
+ assert.Equal(t, "second", items[0].ObjectValue()["name"].StringValue())
+ assert.Equal(t, "first", items[1].ObjectValue()["name"].StringValue())
+}
+
+func TestSetElementMatchScore(t *testing.T) {
+ t.Parallel()
+
+ t.Run("identical objects score all fields", func(t *testing.T) {
+ a := resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("foo"),
+ "value": resource.NewStringProperty("bar"),
+ })
+ b := resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("foo"),
+ "value": resource.NewStringProperty("bar"),
+ })
+ assert.Equal(t, 2, setElementMatchScore(a, b))
+ })
+
+ t.Run("partially matching objects", func(t *testing.T) {
+ a := resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("foo"),
+ "value": resource.NewStringProperty("bar"),
+ })
+ b := resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("foo"),
+ "value": resource.NewStringProperty("baz"),
+ })
+ assert.Equal(t, 1, setElementMatchScore(a, b))
+ })
+
+ t.Run("no matching fields", func(t *testing.T) {
+ a := resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("foo"),
+ })
+ b := resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("bar"),
+ })
+ assert.Equal(t, 0, setElementMatchScore(a, b))
+ })
+
+ t.Run("scalar exact match", func(t *testing.T) {
+ a := resource.NewStringProperty("hello")
+ b := resource.NewStringProperty("hello")
+ assert.Equal(t, 1, setElementMatchScore(a, b))
+ })
+
+ t.Run("scalar mismatch", func(t *testing.T) {
+ a := resource.NewStringProperty("hello")
+ b := resource.NewStringProperty("world")
+ assert.Equal(t, 0, setElementMatchScore(a, b))
+ })
+
+ t.Run("reserved keys are skipped", func(t *testing.T) {
+ a := resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("foo"),
+ resource.PropertyKey(reservedkeys.Defaults): resource.NewArrayProperty(nil),
+ })
+ b := resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("foo"),
+ })
+ assert.Equal(t, 1, setElementMatchScore(a, b))
+ })
+}
+
func TestRefreshExtractInputsFromOutputsListOfObjects(t *testing.T) {
t.Parallel()
@@ -4387,3 +4784,184 @@
assert.Empty(t, assets)
})
}
+
+func TestMakeTerraformInputsTypeSetContentMatching(t *testing.T) {
+ t.Parallel()
+
+ envVarSchema := schema.SchemaMap{
+ "envs": (&schema.Schema{
+ Type: shim.TypeSet,
+ Optional: true,
+ Elem: (&schema.Resource{
+ Schema: schema.SchemaMap{
+ "name": (&schema.Schema{Type: shim.TypeString, Required: true}).Shim(),
+ "value": (&schema.Schema{
+ Type: shim.TypeString,
+ Optional: true,
+ Default: "",
+ }).Shim(),
+ "value_source": (&schema.Schema{
+ Type: shim.TypeList,
+ Optional: true,
+ MaxItems: 1,
+ Elem: (&schema.Resource{
+ Schema: schema.SchemaMap{
+ "secret_key_ref": (&schema.Schema{
+ Type: shim.TypeList,
+ Optional: true,
+ MaxItems: 1,
+ Elem: (&schema.Resource{
+ Schema: schema.SchemaMap{
+ "secret": (&schema.Schema{Type: shim.TypeString, Required: true}).Shim(),
+ "version": (&schema.Schema{Type: shim.TypeString, Optional: true}).Shim(),
+ },
+ }).Shim(),
+ }).Shim(),
+ },
+ }).Shim(),
+ }).Shim(),
+ },
+ }).Shim(),
+ }).Shim(),
+ }
+
+ t.Run("reordered old state does not cross-contaminate oneof fields", func(t *testing.T) {
+ t.Parallel()
+
+ // Old state has secret-ref env var first, plain-value second (provider ordering).
+ olds := resource.PropertyMap{
+ "envs": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("APP_SECRET"),
+ "value": resource.NewStringProperty(""),
+ "valueSource": resource.NewObjectProperty(resource.PropertyMap{
+ "secretKeyRef": resource.NewObjectProperty(resource.PropertyMap{
+ "secret": resource.NewStringProperty("my-secret"),
+ "version": resource.NewStringProperty("latest"),
+ }),
+ }),
+ reservedkeys.Defaults: resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewStringProperty("value"),
+ }),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("ZOO_MODE"),
+ "value": resource.NewStringProperty("production"),
+ }),
+ }),
+ }
+
+ // User inputs have the opposite order: plain-value first, secret-ref second.
+ news := resource.PropertyMap{
+ "envs": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("ZOO_MODE"),
+ "value": resource.NewStringProperty("production"),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("APP_SECRET"),
+ "valueSource": resource.NewObjectProperty(resource.PropertyMap{
+ "secretKeyRef": resource.NewObjectProperty(resource.PropertyMap{
+ "secret": resource.NewStringProperty("my-secret"),
+ "version": resource.NewStringProperty("latest"),
+ }),
+ }),
+ }),
+ }),
+ }
+
+ result, _, err := makeTerraformInputsForConfig(olds, news, envVarSchema, nil)
+ require.NoError(t, err)
+
+ envs, ok := result["envs"]
+ require.True(t, ok)
+ envsArr := envs.([]interface{})
+ require.Len(t, envsArr, 2)
+
+ for _, env := range envsArr {
+ envMap := env.(map[string]interface{})
+ name := envMap["name"].(string)
+ hasValue := envMap["value"] != nil && envMap["value"] != ""
+ hasValueSource := envMap["value_source"] != nil
+
+ // The critical assertion: no env var should have both a non-empty
+ // value and a value_source. That would trigger a GCP oneof violation.
+ if hasValueSource {
+ assert.Falsef(t, hasValue,
+ "env %s has both value=%v and value_source — oneof violation",
+ name, envMap["value"])
+ }
+ }
+ })
+
+ t.Run("element added shifts positions without contamination", func(t *testing.T) {
+ t.Parallel()
+
+ // Old state: two env vars.
+ olds := resource.PropertyMap{
+ "envs": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("DB_PASSWORD"),
+ "value": resource.NewStringProperty(""),
+ "valueSource": resource.NewObjectProperty(resource.PropertyMap{
+ "secretKeyRef": resource.NewObjectProperty(resource.PropertyMap{
+ "secret": resource.NewStringProperty("db-pass"),
+ "version": resource.NewStringProperty("1"),
+ }),
+ }),
+ reservedkeys.Defaults: resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewStringProperty("value"),
+ }),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("LOG_LEVEL"),
+ "value": resource.NewStringProperty("INFO"),
+ }),
+ }),
+ }
+
+ // User adds a new env var at the beginning, shifting positions.
+ news := resource.PropertyMap{
+ "envs": resource.NewArrayProperty([]resource.PropertyValue{
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("APP_NAME"),
+ "value": resource.NewStringProperty("myapp"),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("DB_PASSWORD"),
+ "valueSource": resource.NewObjectProperty(resource.PropertyMap{
+ "secretKeyRef": resource.NewObjectProperty(resource.PropertyMap{
+ "secret": resource.NewStringProperty("db-pass"),
+ "version": resource.NewStringProperty("1"),
+ }),
+ }),
+ }),
+ resource.NewObjectProperty(resource.PropertyMap{
+ "name": resource.NewStringProperty("LOG_LEVEL"),
+ "value": resource.NewStringProperty("INFO"),
+ }),
+ }),
+ }
+
+ result, _, err := makeTerraformInputsForConfig(olds, news, envVarSchema, nil)
+ require.NoError(t, err)
+
+ envs, ok := result["envs"]
+ require.True(t, ok)
+ envsArr := envs.([]interface{})
+ require.Len(t, envsArr, 3)
+
+ for _, env := range envsArr {
+ envMap := env.(map[string]interface{})
+ name := envMap["name"].(string)
+ hasValue := envMap["value"] != nil && envMap["value"] != ""
+ hasValueSource := envMap["value_source"] != nil
+
+ if hasValueSource {
+ assert.Falsef(t, hasValue,
+ "env %s has both value=%v and value_source — oneof violation",
+ name, envMap["value"])
+ }
+ }
+ })
+}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment