Skip to content

Commit fbd42ce

Browse files
corymhallcodex
andauthored
Fix ArgoTieredCaching state upgrade (#1581)
## Summary - add a narrow pre-state upgrade hook for old Pulumi-shaped `ArgoTieredCaching` state - mark only `{value, zoneId}` schema-version-0 state as upstream schema version 500 so the legacy `cloudflare_argo` migration is skipped - add credential-free regression coverage for v6.12-shaped exported state plus a schema-version reminder test Fixes #1575 ## Background `cloudflare_argo_tiered_caching` has an upstream Terraform migration edge case across these provider versions. State created by the older v5 provider can remain at schema version 0. Newer upstream versions now interpret schema version 0 for this resource as legacy `cloudflare_argo` state and run a migration that expects the old `tiered_caching` field. That is not the shape Pulumi wrote for `cloudflare:index/argoTieredCaching:ArgoTieredCaching`. Pulumi-created state from v6.12 already has the current resource shape, using `value` and `zoneId`, but lacks the newer upstream schema-version marker. On a direct upgrade to newer provider versions, upstream's legacy migration sees schema version 0, looks for `tiered_caching`, and fails. We confirmed the same direct-upgrade issue exists in Terraform: the upstream acceptance harness succeeds when it steps through an intermediate provider version, but a direct published-provider upgrade reproduces the `tiered_caching attribute is required` failure. So this is not Pulumi failing to run a migration; it is the direct upstream upgrade path requiring an intermediate schema bump. ## Approach This PR works around the issue locally for Pulumi-created state. The hook only applies when all of these are true: - the prior schema version is 0 - the state is already Pulumi/current-shaped, with `value` and `zoneId` - the state does not contain legacy `tiered_caching` For that narrow shape, the hook returns schema version 500 unchanged. That tells the Terraform Framework provider the state is already at the current `cloudflare_argo_tiered_caching` schema and avoids running the wrong legacy `cloudflare_argo` migration. Legacy Terraform-shaped state is intentionally left alone. ## Risk The main risk is future upstream drift. The hook hardcodes schema version 500 because that is the current upstream schema version for this resource. A reminder test fails if upstream bumps this resource's schema version so we revisit whether the hook should change or be removed. ## Testing - `cd provider && mise exec -- go test . -run 'TestArgoTieredCaching' -count=1` Co-authored-by: Codex <noreply@openai.com>
1 parent 4d4e717 commit fbd42ce

5 files changed

Lines changed: 274 additions & 0 deletions

File tree

provider/provider_program_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ package cloudflare
66
import (
77
"context"
88
"encoding/json"
9+
"fmt"
910
"os"
11+
"strings"
1012
"testing"
1113

1214
_ "embed"
@@ -88,6 +90,105 @@ func testProgram(t *testing.T, dir string, opts ...opttest.Option) *pulumitest.P
8890
return pt
8991
}
9092

93+
func testProgramNoCloudflareConfig(t *testing.T, dir string, opts ...opttest.Option) *pulumitest.PulumiTest {
94+
rpFactory := providers.ResourceProviderFactory(providerFactory)
95+
opts = append(opts, opttest.AttachProvider(providerName, rpFactory), opttest.SkipInstall())
96+
return pulumitest.NewPulumiTest(t, dir, opts...)
97+
}
98+
99+
func pulumiCommandEnv(pt *pulumitest.PulumiTest) []string {
100+
workspace := pt.CurrentStack().Workspace()
101+
env := []string{"PULUMI_DEBUG_COMMANDS=true"}
102+
if pulumiHome := workspace.PulumiHome(); pulumiHome != "" {
103+
env = append(env, "PULUMI_HOME="+pulumiHome)
104+
}
105+
for k, v := range workspace.GetEnvVars() {
106+
env = append(env, strings.Join([]string{k, v}, "="))
107+
}
108+
return env
109+
}
110+
111+
func importStackWithDisabledIntegrity(t *testing.T, pt *pulumitest.PulumiTest, source apitype.UntypedDeployment) {
112+
t.Helper()
113+
stack := pt.CurrentStack()
114+
require.NotNil(t, stack)
115+
116+
f, err := os.CreateTemp(t.TempDir(), "stack-*.json")
117+
require.NoError(t, err)
118+
defer func() { require.NoError(t, f.Close()) }()
119+
120+
require.NoError(t, json.NewEncoder(f).Encode(source))
121+
122+
workspace := stack.Workspace()
123+
stdout, stderr, _, err := workspace.PulumiCommand().Run(
124+
pt.Context(),
125+
workspace.WorkDir(),
126+
nil,
127+
nil,
128+
nil,
129+
pulumiCommandEnv(pt),
130+
"--disable-integrity-checking",
131+
"stack",
132+
"import",
133+
"--file",
134+
f.Name(),
135+
"--stack",
136+
stack.Name(),
137+
)
138+
require.NoError(t, err, fmt.Sprintf("stdout:\n%s\nstderr:\n%s", stdout, stderr))
139+
}
140+
141+
func previewWithDisabledIntegrity(t *testing.T, pt *pulumitest.PulumiTest) (string, string, error) {
142+
t.Helper()
143+
stack := pt.CurrentStack()
144+
require.NotNil(t, stack)
145+
146+
workspace := stack.Workspace()
147+
stdout, stderr, _, err := workspace.PulumiCommand().Run(
148+
pt.Context(),
149+
workspace.WorkDir(),
150+
nil,
151+
nil,
152+
nil,
153+
pulumiCommandEnv(pt),
154+
"--disable-integrity-checking",
155+
"preview",
156+
"--non-interactive",
157+
"--diff",
158+
"--stack",
159+
stack.Name(),
160+
)
161+
return stdout, stderr, err
162+
}
163+
164+
func withArgoTieredCachingSchemaVersion(
165+
t *testing.T, source apitype.UntypedDeployment, version string,
166+
) apitype.UntypedDeployment {
167+
t.Helper()
168+
var deployment map[string]interface{}
169+
require.NoError(t, json.Unmarshal(source.Deployment, &deployment))
170+
resources, ok := deployment["resources"].([]interface{})
171+
require.True(t, ok)
172+
found := false
173+
for _, rawResource := range resources {
174+
res, ok := rawResource.(map[string]interface{})
175+
require.True(t, ok)
176+
if res["type"] != "cloudflare:index/argoTieredCaching:ArgoTieredCaching" {
177+
continue
178+
}
179+
found = true
180+
outputs, ok := res["outputs"].(map[string]interface{})
181+
require.True(t, ok)
182+
outputs["__meta"] = fmt.Sprintf(`{"schema_version":"%s"}`, version)
183+
break
184+
}
185+
require.True(t, found, "did not find ArgoTieredCaching resource in test state")
186+
updatedDeployment, err := json.Marshal(deployment)
187+
require.NoError(t, err)
188+
source.Deployment = updatedDeployment
189+
return source
190+
}
191+
91192
func testUpgrade(
92193
t *testing.T, dir1 string, opts ...optproviderupgrade.PreviewProviderUpgradeOpt,
93194
) auto.PreviewResult {
@@ -152,6 +253,29 @@ func TestZeroTrustAccessApplicationFromState(t *testing.T) {
152253
pt.Preview(t)
153254
}
154255

256+
func TestArgoTieredCachingFromV612State(t *testing.T) {
257+
state, err := os.ReadFile("testdata/argo_tiered_caching_state_v6_12.json")
258+
require.NoError(t, err)
259+
depl := apitype.UntypedDeployment{}
260+
require.NoError(t, json.Unmarshal(state, &depl))
261+
262+
t.Run("direct upgrade skips legacy argo migration for Pulumi state", func(t *testing.T) {
263+
pt := testProgramNoCloudflareConfig(t, "test-programs/argo_tiered_caching_state",
264+
opttest.NewStackOptions(optnewstack.DisableAutoDestroy()))
265+
importStackWithDisabledIntegrity(t, pt, depl)
266+
stdout, stderr, err := previewWithDisabledIntegrity(t, pt)
267+
require.NoError(t, err, fmt.Sprintf("stdout:\n%s\nstderr:\n%s", stdout, stderr))
268+
})
269+
270+
t.Run("schema version bump avoids legacy argo migration", func(t *testing.T) {
271+
pt := testProgramNoCloudflareConfig(t, "test-programs/argo_tiered_caching_state",
272+
opttest.NewStackOptions(optnewstack.DisableAutoDestroy()))
273+
importStackWithDisabledIntegrity(t, pt, withArgoTieredCachingSchemaVersion(t, depl, "500"))
274+
stdout, stderr, err := previewWithDisabledIntegrity(t, pt)
275+
require.NoError(t, err, fmt.Sprintf("stdout:\n%s\nstderr:\n%s", stdout, stderr))
276+
})
277+
}
278+
155279
func TestRuleSetHeadersUpgrade(t *testing.T) {
156280
testUpgrade(
157281
t, "test-programs/ruleset_headers/ruleset_headers_v5",

provider/resources.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ func Provider() info.Provider {
125125
ComputeID: delegateID("imageId"),
126126
},
127127

128+
"cloudflare_argo_tiered_caching": {
129+
PreStateUpgradeHook: argoTieredCachingPreStateUpgradeHook,
130+
},
131+
128132
"cloudflare_ruleset": {
129133
Tok: "cloudflare:index/ruleset:Ruleset",
130134
PreStateUpgradeHook: func(
@@ -792,6 +796,37 @@ func resetMigratedResourcesSchemaVersion(prov *info.Provider) {
792796
}
793797
}
794798

799+
// argoTieredCachingPreStateUpgradeHook handles Pulumi state written before the
800+
// upstream resource had a current schema marker. Upstream now treats schema
801+
// version 0 as legacy Terraform cloudflare_argo state and expects
802+
// tiered_caching, but old Pulumi ArgoTieredCaching state is already shaped like
803+
// the current resource. Mark that Pulumi-shaped state as version 500 so the
804+
// wrong legacy migration is skipped.
805+
func argoTieredCachingPreStateUpgradeHook(
806+
args info.PreStateUpgradeHookArgs,
807+
) (int64, resource.PropertyMap, error) {
808+
if args.PriorStateSchemaVersion == 0 && isPulumiArgoTieredCachingState(args.PriorState) {
809+
return 500, args.PriorState, nil
810+
}
811+
return args.PriorStateSchemaVersion, args.PriorState, nil
812+
}
813+
814+
// isPulumiArgoTieredCachingState narrowly identifies old Pulumi-created
815+
// cloudflare_argo_tiered_caching state. The camelCase zoneId key distinguishes
816+
// it from Terraform's legacy snake_case cloudflare_argo state, and the absence
817+
// of tiered_caching keeps the real legacy migration path intact.
818+
func isPulumiArgoTieredCachingState(state resource.PropertyMap) bool {
819+
value, hasValue := state["value"]
820+
zoneID, hasZoneID := state["zoneId"]
821+
_, hasTerraformTieredCaching := state["tiered_caching"]
822+
_, hasPulumiTieredCaching := state["tieredCaching"]
823+
824+
return hasValue && value.IsString() &&
825+
hasZoneID && zoneID.IsString() &&
826+
!hasTerraformTieredCaching &&
827+
!hasPulumiTieredCaching
828+
}
829+
795830
func delegateID(pulumiField resource.PropertyKey) tfbridge.ComputeID {
796831
repoURL := "https://github.com/pulumi/pulumi-cloudflare"
797832
d := tfbridge.DelegateIDField(pulumiField, "cloudflare", repoURL)

provider/resources_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/stretchr/testify/assert"
88
"github.com/stretchr/testify/require"
99

10+
"github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge"
1011
shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim"
1112
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
1213
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
@@ -80,6 +81,44 @@ func TestZeroTrustAccessApplicationVersionReminder(t *testing.T) {
8081
"custom Pulumi PreStateUpgradeHook needs to be revisited or possibly dropped")
8182
}
8283

84+
func TestArgoTieredCachingVersionReminder(t *testing.T) {
85+
version.Version = "0.0.4"
86+
p := Provider()
87+
r := p.P.ResourcesMap().Get("cloudflare_argo_tiered_caching")
88+
// See https://github.com/pulumi/pulumi-cloudflare/issues/1575
89+
assert.Equalf(t, 500, r.SchemaVersion(),
90+
"Reminder: cloudflare_argo_tiered_caching advanced schema version from 500 and "+
91+
"custom Pulumi PreStateUpgradeHook needs to be revisited or possibly dropped")
92+
}
93+
94+
func TestArgoTieredCachingPreStateUpgradeHook(t *testing.T) {
95+
pulumiState := resource.PropertyMap{
96+
"value": resource.NewStringProperty("on"),
97+
"zoneId": resource.NewStringProperty("00000000000000000000000000000000"),
98+
}
99+
version, state, err := argoTieredCachingPreStateUpgradeHook(
100+
tfbridge.PreStateUpgradeHookArgs{
101+
PriorStateSchemaVersion: 0,
102+
PriorState: pulumiState,
103+
})
104+
require.NoError(t, err)
105+
assert.Equal(t, int64(500), version)
106+
assert.Equal(t, pulumiState, state)
107+
108+
legacyArgoState := resource.PropertyMap{
109+
"tiered_caching": resource.NewStringProperty("on"),
110+
"zone_id": resource.NewStringProperty("00000000000000000000000000000000"),
111+
}
112+
version, state, err = argoTieredCachingPreStateUpgradeHook(
113+
tfbridge.PreStateUpgradeHookArgs{
114+
PriorStateSchemaVersion: 0,
115+
PriorState: legacyArgoState,
116+
})
117+
require.NoError(t, err)
118+
assert.Equal(t, int64(0), version)
119+
assert.Equal(t, legacyArgoState, state)
120+
}
121+
83122
func Test_delegateID(t *testing.T) {
84123

85124
type testCase struct {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name: argo-tiered-caching-state
2+
runtime: yaml
3+
4+
resources:
5+
argo:
6+
type: cloudflare:ArgoTieredCaching
7+
properties:
8+
zoneId: 00000000000000000000000000000000
9+
value: "on"
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"version": 3,
3+
"deployment": {
4+
"manifest": {
5+
"time": "2026-05-18T00:00:00Z",
6+
"magic": "0000000000000000000000000000000000000000000000000000000000000000",
7+
"version": "v3.228.0"
8+
},
9+
"secrets_providers": {
10+
"type": "passphrase",
11+
"state": {
12+
"salt": "v1:O1t5suqMRIo=:v1:7DqVMCoY+uij+EZC:Bc3SzKdajmbRZU5OXZWfP0oEcpUIJw=="
13+
}
14+
},
15+
"resources": [
16+
{
17+
"urn": "urn:pulumi:test::argo-tiered-caching-state::pulumi:pulumi:Stack::argo-tiered-caching-state-test",
18+
"custom": false,
19+
"type": "pulumi:pulumi:Stack",
20+
"outputs": {}
21+
},
22+
{
23+
"urn": "urn:pulumi:test::argo-tiered-caching-state::pulumi:providers:cloudflare::default",
24+
"custom": true,
25+
"id": "00000000-0000-0000-0000-000000000001",
26+
"type": "pulumi:providers:cloudflare",
27+
"inputs": {
28+
"apiClientLogging": "false",
29+
"maxBackoff": "30",
30+
"minBackoff": "1",
31+
"retries": "3",
32+
"rps": "4"
33+
},
34+
"outputs": {
35+
"apiClientLogging": "false",
36+
"maxBackoff": "30",
37+
"minBackoff": "1",
38+
"retries": "3",
39+
"rps": "4"
40+
}
41+
},
42+
{
43+
"urn": "urn:pulumi:test::argo-tiered-caching-state::cloudflare:index/argoTieredCaching:ArgoTieredCaching::argo",
44+
"custom": true,
45+
"id": "00000000000000000000000000000000",
46+
"type": "cloudflare:index/argoTieredCaching:ArgoTieredCaching",
47+
"inputs": {
48+
"value": "on",
49+
"zoneId": "00000000000000000000000000000000"
50+
},
51+
"outputs": {
52+
"editable": false,
53+
"id": "00000000000000000000000000000000",
54+
"modifiedOn": "2025-08-15T09:49:33Z",
55+
"value": "on",
56+
"zoneId": "00000000000000000000000000000000"
57+
},
58+
"parent": "urn:pulumi:test::argo-tiered-caching-state::pulumi:pulumi:Stack::argo-tiered-caching-state-test",
59+
"provider": "urn:pulumi:test::argo-tiered-caching-state::pulumi:providers:cloudflare::default::00000000-0000-0000-0000-000000000001",
60+
"propertyDependencies": {
61+
"value": null,
62+
"zoneId": null
63+
}
64+
}
65+
]
66+
}
67+
}

0 commit comments

Comments
 (0)