Created
March 23, 2026 20:35
-
-
Save Kazy/1f8d5867e31f043621b21add95e48927 to your computer and use it in GitHub Desktop.
Pulumi TF Bridge CloudRun fix patch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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