Skip to content

Commit 3d9c23a

Browse files
Fix bridge not running state upgrades from 0 -> 1 (#2081)
fixes #2039 The bridge assumes that a missing `schema_version` in the meta state means "current version". That is wrong as we only miss the `schema_version` if it is 0 - we write all non-zero versions to the state. Any resources created under a v0 TF schema version will not save the the v0 in the state. This means that if this resource then gets a TF state upgrade, we will assume we are already on the new version and NOT run the state upgrade as we should. This PR changes the assumption to mean missing `schema_version` means "v0". Note that `TestUpgradeInputsStringBasicNonZeroVersionSame` passed before, which shows that any existing resources which had state upgrades defined should have the correct state version in their meta property. It also adds tests around this both for PRC and non-PRC. The feature is flagged behind a provider flag, supplied in `ProviderInfo` - the feature flag mechanism is adapted from the sdkv2 feature flag mechanism.
1 parent 674cf5b commit 3d9c23a

File tree

11 files changed

+347
-95
lines changed

11 files changed

+347
-95
lines changed

pkg/tests/cross-tests/input_check.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type inputTestCase struct {
2828

2929
Config any
3030
ObjectType *tftypes.Object
31+
32+
DisablePlanResourceChange bool
3133
}
3234

3335
// Adapted from diff_check.go

pkg/tests/cross-tests/upgrade_state_check.go

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,66 @@ package crosstests
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strconv"
610

11+
"github.com/hashicorp/terraform-plugin-go/tftypes"
712
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
813
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tests/internal/pulcheck"
9-
"github.com/stretchr/testify/assert"
14+
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
1015
"github.com/stretchr/testify/require"
16+
"gotest.tools/v3/assert"
1117
)
1218

13-
func runPulumiUpgrade(t T, res1, res2 *schema.Resource, config any) {
14-
prov1 := pulcheck.BridgedProvider(t, defProviderShortName, map[string]*schema.Resource{defRtype: res1})
15-
prov2 := pulcheck.BridgedProvider(t, defProviderShortName, map[string]*schema.Resource{defRtype: res2})
19+
type upgradeStateTestCase struct {
20+
// Schema for the resource under test
21+
Resource *schema.Resource
22+
23+
Config1 any
24+
Config2 any
25+
ExpectEqual bool
26+
ObjectType *tftypes.Object
27+
28+
DisablePlanResourceChange bool
29+
}
30+
31+
func getVersionInState(t T, stack apitype.UntypedDeployment) int {
32+
data, err := stack.Deployment.MarshalJSON()
33+
require.NoError(t, err)
34+
35+
var stateMap map[string]interface{}
36+
err = json.Unmarshal(data, &stateMap)
37+
require.NoError(t, err)
38+
39+
resourcesList := stateMap["resources"].([]interface{})
40+
require.Len(t, resourcesList, 3)
41+
testResState := resourcesList[2].(map[string]interface{})
42+
resOutputs := testResState["outputs"].(map[string]interface{})
43+
metaVar := resOutputs["__meta"]
44+
if metaVar == nil {
45+
// If the resource does not have a meta field, assume the schema version is 0.
46+
return 0
47+
}
48+
meta := metaVar.(string)
49+
var metaMap map[string]interface{}
50+
err = json.Unmarshal([]byte(meta), &metaMap)
51+
require.NoError(t, err)
52+
schemaVersion, err := strconv.ParseInt(metaMap["schema_version"].(string), 10, 64)
53+
require.NoError(t, err)
54+
return int(schemaVersion)
55+
}
56+
57+
func runPulumiUpgrade(t T, res1, res2 *schema.Resource, config1, config2 any, disablePlanResourceChange bool) (int, int) {
58+
opts := []pulcheck.BridgedProviderOpt{}
59+
if disablePlanResourceChange {
60+
opts = append(opts, pulcheck.DisablePlanResourceChange())
61+
}
62+
63+
prov1 := pulcheck.BridgedProvider(t, defProviderShortName, map[string]*schema.Resource{defRtype: res1}, opts...)
64+
prov2 := pulcheck.BridgedProvider(t, defProviderShortName, map[string]*schema.Resource{defRtype: res2}, opts...)
1665

1766
pd := &pulumiDriver{
1867
name: defProviderShortName,
@@ -21,18 +70,29 @@ func runPulumiUpgrade(t T, res1, res2 *schema.Resource, config any) {
2170
objectType: nil,
2271
}
2372

24-
yamlProgram := pd.generateYAML(t, prov1.P.ResourcesMap(), config)
73+
yamlProgram := pd.generateYAML(t, prov1.P.ResourcesMap(), config1)
2574
pt := pulcheck.PulCheck(t, prov1, string(yamlProgram))
26-
2775
pt.Up()
76+
stack := pt.ExportStack()
77+
schemaVersion1 := getVersionInState(t, stack)
78+
79+
yamlProgram = pd.generateYAML(t, prov2.P.ResourcesMap(), config2)
80+
p := filepath.Join(pt.CurrentStack().Workspace().WorkDir(), "Pulumi.yaml")
81+
err := os.WriteFile(p, yamlProgram, 0o600)
82+
require.NoErrorf(t, err, "writing Pulumi.yaml")
2883

2984
handle, err := pulcheck.StartPulumiProvider(context.Background(), defProviderShortName, defProviderVer, prov2)
3085
require.NoError(t, err)
3186
pt.CurrentStack().Workspace().SetEnvVar("PULUMI_DEBUG_PROVIDERS", fmt.Sprintf("%s:%d", defProviderShortName, handle.Port))
3287
pt.Up()
88+
89+
stack = pt.ExportStack()
90+
schemaVersion2 := getVersionInState(t, stack)
91+
92+
return schemaVersion1, schemaVersion2
3393
}
3494

35-
func runUpgradeStateInputCheck(t T, tc inputTestCase) {
95+
func runUpgradeStateInputCheck(t T, tc upgradeStateTestCase) {
3696
upgrades := make([]schema.StateUpgrader, 0)
3797
for i := 0; i < tc.Resource.SchemaVersion; i++ {
3898
upgrades = append(upgrades, schema.StateUpgrader{
@@ -72,13 +132,32 @@ func runUpgradeStateInputCheck(t T, tc inputTestCase) {
72132
tfwd := t.TempDir()
73133

74134
tfd := newTfDriver(t, tfwd, defProviderShortName, defRtype, tc.Resource)
75-
_ = tfd.writePlanApply(t, tc.Resource.Schema, defRtype, "example", tc.Config)
135+
_ = tfd.writePlanApply(t, tc.Resource.Schema, defRtype, "example", tc.Config1)
76136

77137
tfd2 := newTfDriver(t, tfwd, defProviderShortName, defRtype, &upgradeRes)
78-
_ = tfd2.writePlanApply(t, tc.Resource.Schema, defRtype, "example", tc.Config)
79-
80-
runPulumiUpgrade(t, tc.Resource, &upgradeRes, tc.Config)
81-
82-
assert.Len(t, upgradeRawStates, 2)
83-
assertValEqual(t, "UpgradeRawState", upgradeRawStates[0], upgradeRawStates[1])
138+
_ = tfd2.writePlanApply(t, tc.Resource.Schema, defRtype, "example", tc.Config2)
139+
140+
schemaVersion1, schemaVersion2 := runPulumiUpgrade(t, tc.Resource, &upgradeRes, tc.Config1, tc.Config2, tc.DisablePlanResourceChange)
141+
142+
if tc.ExpectEqual {
143+
assert.Equal(t, schemaVersion1, tc.Resource.SchemaVersion)
144+
// We never upgrade the state to the new version.
145+
// TODO: should we?
146+
147+
require.Len(t, upgradeRawStates, 2)
148+
if len(upgradeRawStates) != 2 {
149+
return
150+
}
151+
assertValEqual(t, "UpgradeRawState", upgradeRawStates[0], upgradeRawStates[1])
152+
153+
} else {
154+
assert.Equal(t, schemaVersion1, tc.Resource.SchemaVersion)
155+
assert.Equal(t, schemaVersion2, upgradeRes.SchemaVersion)
156+
require.Len(t, upgradeRawStates, 4)
157+
if len(upgradeRawStates) != 4 {
158+
return
159+
}
160+
assertValEqual(t, "UpgradeRawState", upgradeRawStates[0], upgradeRawStates[2])
161+
assertValEqual(t, "UpgradeRawState", upgradeRawStates[1], upgradeRawStates[3])
162+
}
84163
}
Lines changed: 114 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package crosstests
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/hashicorp/terraform-plugin-go/tftypes"
@@ -9,74 +10,121 @@ import (
910

1011
func TestUpgradeInputsStringBasic(t *testing.T) {
1112
skipUnlessLinux(t)
12-
t.Skipf("TODO[pulumi/pulumi-terraform-bridge#2039] - Zero schema version does not work")
13-
runUpgradeStateInputCheck(t, inputTestCase{
14-
Resource: &schema.Resource{
15-
Schema: map[string]*schema.Schema{
16-
"f0": {
17-
Type: schema.TypeString,
18-
Optional: true,
19-
},
13+
res := &schema.Resource{
14+
Schema: map[string]*schema.Schema{
15+
"f0": {
16+
Type: schema.TypeString,
17+
Optional: true,
2018
},
2119
},
22-
Config: tftypes.NewValue(tftypes.Object{
20+
}
21+
22+
configVal := func(val string) tftypes.Value {
23+
return tftypes.NewValue(tftypes.Object{
2324
AttributeTypes: map[string]tftypes.Type{
2425
"f0": tftypes.String,
2526
},
2627
}, map[string]tftypes.Value{
27-
"f0": tftypes.NewValue(tftypes.String, "val"),
28-
}),
29-
})
28+
"f0": tftypes.NewValue(tftypes.String, val),
29+
})
30+
}
31+
for _, PRC := range []bool{true, false} {
32+
t.Run(fmt.Sprintf("PRC=%v", PRC), func(t *testing.T) {
33+
t.Run("same", func(t *testing.T) {
34+
runUpgradeStateInputCheck(t, upgradeStateTestCase{
35+
Resource: res,
36+
Config1: configVal("val"),
37+
Config2: configVal("val"),
38+
DisablePlanResourceChange: !PRC,
39+
ExpectEqual: true,
40+
})
41+
})
42+
43+
t.Run("different", func(t *testing.T) {
44+
runUpgradeStateInputCheck(t, upgradeStateTestCase{
45+
Resource: res,
46+
Config1: configVal("val1"),
47+
Config2: configVal("val2"),
48+
DisablePlanResourceChange: !PRC,
49+
})
50+
})
51+
})
52+
}
3053
}
3154

3255
func TestUpgradeInputsStringBasicNonZeroVersion(t *testing.T) {
3356
skipUnlessLinux(t)
3457

35-
runUpgradeStateInputCheck(t, inputTestCase{
36-
Resource: &schema.Resource{
37-
Schema: map[string]*schema.Schema{
38-
"f0": {
39-
Type: schema.TypeString,
40-
Optional: true,
41-
},
58+
res := &schema.Resource{
59+
Schema: map[string]*schema.Schema{
60+
"f0": {
61+
Type: schema.TypeString,
62+
Optional: true,
4263
},
43-
SchemaVersion: 1,
4464
},
45-
Config: tftypes.NewValue(tftypes.Object{
65+
SchemaVersion: 1,
66+
}
67+
68+
configVal := func(val string) tftypes.Value {
69+
return tftypes.NewValue(tftypes.Object{
4670
AttributeTypes: map[string]tftypes.Type{
4771
"f0": tftypes.String,
4872
},
4973
}, map[string]tftypes.Value{
50-
"f0": tftypes.NewValue(tftypes.String, "val"),
51-
}),
52-
})
74+
"f0": tftypes.NewValue(tftypes.String, val),
75+
})
76+
}
77+
for _, PRC := range []bool{true, false} {
78+
t.Run(fmt.Sprintf("PRC=%v", PRC), func(t *testing.T) {
79+
t.Run("same", func(t *testing.T) {
80+
runUpgradeStateInputCheck(t, upgradeStateTestCase{
81+
Resource: res,
82+
Config1: configVal("val"),
83+
Config2: configVal("val"),
84+
DisablePlanResourceChange: !PRC,
85+
ExpectEqual: true,
86+
})
87+
})
88+
89+
t.Run("different", func(t *testing.T) {
90+
runUpgradeStateInputCheck(t, upgradeStateTestCase{
91+
Resource: res,
92+
Config1: configVal("val1"),
93+
Config2: configVal("val2"),
94+
DisablePlanResourceChange: !PRC,
95+
})
96+
})
97+
})
98+
}
5399
}
54100

55101
func TestUpgradeInputsObjectBasic(t *testing.T) {
56102
skipUnlessLinux(t)
57-
t.Skipf("TODO[pulumi/pulumi-terraform-bridge#2039] - Zero schema version does not work")
103+
104+
res := &schema.Resource{
105+
Schema: map[string]*schema.Schema{
106+
"f0": {
107+
Required: true,
108+
Type: schema.TypeList,
109+
MaxItems: 1,
110+
Elem: &schema.Resource{
111+
Schema: map[string]*schema.Schema{
112+
"x": {Optional: true, Type: schema.TypeString},
113+
},
114+
},
115+
},
116+
},
117+
}
118+
58119
t1 := tftypes.Object{
59120
AttributeTypes: map[string]tftypes.Type{
60121
"x": tftypes.String,
61122
},
62123
}
63124
t0 := tftypes.List{ElementType: t1}
64-
runUpgradeStateInputCheck(t, inputTestCase{
65-
Resource: &schema.Resource{
66-
Schema: map[string]*schema.Schema{
67-
"f0": {
68-
Required: true,
69-
Type: schema.TypeList,
70-
MaxItems: 1,
71-
Elem: &schema.Resource{
72-
Schema: map[string]*schema.Schema{
73-
"x": {Optional: true, Type: schema.TypeString},
74-
},
75-
},
76-
},
77-
},
78-
},
79-
Config: tftypes.NewValue(
125+
126+
configVal := func(val string) tftypes.Value {
127+
return tftypes.NewValue(
80128
tftypes.Object{
81129
AttributeTypes: map[string]tftypes.Type{
82130
"f0": t0,
@@ -87,11 +135,33 @@ func TestUpgradeInputsObjectBasic(t *testing.T) {
87135
[]tftypes.Value{
88136
tftypes.NewValue(t1,
89137
map[string]tftypes.Value{
90-
"x": tftypes.NewValue(tftypes.String, "ok"),
138+
"x": tftypes.NewValue(tftypes.String, val),
91139
}),
92140
},
93141
),
94142
},
95-
),
96-
})
143+
)
144+
}
145+
for _, PRC := range []bool{true, false} {
146+
t.Run(fmt.Sprintf("PRC=%v", PRC), func(t *testing.T) {
147+
t.Run("same", func(t *testing.T) {
148+
runUpgradeStateInputCheck(t, upgradeStateTestCase{
149+
Resource: res,
150+
Config1: configVal("val"),
151+
Config2: configVal("val"),
152+
DisablePlanResourceChange: !PRC,
153+
ExpectEqual: true,
154+
})
155+
})
156+
157+
t.Run("different", func(t *testing.T) {
158+
runUpgradeStateInputCheck(t, upgradeStateTestCase{
159+
Resource: res,
160+
Config1: configVal("val1"),
161+
Config2: configVal("val2"),
162+
DisablePlanResourceChange: !PRC,
163+
})
164+
})
165+
})
166+
}
97167
}

pkg/tests/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/pulumi/pulumi-terraform-bridge/v3 v3.80.0
1616
github.com/stretchr/testify v1.9.0
1717
gotest.tools v2.2.0+incompatible
18+
gotest.tools/v3 v3.0.3
1819
pgregory.net/rapid v0.6.1
1920
)
2021

pkg/tests/go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2515,6 +2515,7 @@ golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBn
25152515
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
25162516
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
25172517
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
2518+
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
25182519
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
25192520
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
25202521
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

pkg/tests/internal/pulcheck/pulcheck.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,11 @@ func BridgedProvider(t T, providerName string, resMap map[string]*schema.Resourc
133133
))
134134

135135
provider := tfbridge.ProviderInfo{
136-
P: shimProvider,
137-
Name: providerName,
138-
Version: "0.0.1",
139-
MetadataInfo: &tfbridge.MetadataInfo{},
136+
P: shimProvider,
137+
Name: providerName,
138+
Version: "0.0.1",
139+
MetadataInfo: &tfbridge.MetadataInfo{},
140+
EnableZeroDefaultSchemaVersion: true,
140141
}
141142
makeToken := func(module, name string) (string, error) {
142143
return tokens.MakeStandard(providerName)(module, name)

0 commit comments

Comments
 (0)