Skip to content

Commit 0307d26

Browse files
fix: cloudflare_workers_script.assets.config.run_worker_first accepts list input
- This property can either be a boolean or list of strings, the API accepts both - Update resource to accept list of strings in addition to boolean values
1 parent 618db3c commit 0307d26

File tree

8 files changed

+401
-16
lines changed

8 files changed

+401
-16
lines changed

docs/resources/workers_script.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ Available values: "auto-trailing-slash", "force-trailing-slash", "drop-trailing-
159159
- `not_found_handling` (String) Determines the response when a request does not match a static asset, and there is no Worker script.
160160
Available values: "none", "404-page", "single-page-application".
161161
- `redirects` (String) The contents of a _redirects file (used to apply redirects or proxy paths ahead of asset serving).
162-
- `run_worker_first` (Boolean) When true, requests will always invoke the Worker script. Otherwise, attempt to serve an asset matching the request, falling back to the Worker script.
162+
- `run_worker_first` (Boolean, List of String) When a boolean true, requests will always invoke the Worker script. Otherwise, attempt to serve an asset matching the request, falling back to the Worker script. When a list of strings, contains path rules to control routing to either the Worker or assets. Glob (*) and negative (!) rules are supported. Rules must start with either '/' or '!/'. At least one non-negative rule must be provided, and negative rules have higher precedence than non-negative rules.
163163
- `serve_directly` (Boolean, Deprecated) When true and the incoming request matches an asset, that will be served instead of invoking the Worker script. When false, requests will always invoke the Worker script.
164164

165165

internal/acctest/acctest.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,12 @@ func TestAccPreCheck_Credentials(t *testing.T) {
5858
userServiceKey := os.Getenv(consts.APIUserServiceKeyEnvVarKey)
5959

6060
if apiToken == "" && apiKey == "" && userServiceKey == "" {
61-
t.Fatal("valid credentials are required for this acceptance test.")
61+
t.Fatalf(
62+
"valid credentials are required for this acceptance test: one of %s, %s, or %s must be set",
63+
consts.APIKeyEnvVarKey,
64+
consts.APITokenEnvVarKey,
65+
consts.APIUserServiceKeyEnvVarKey,
66+
)
6267
}
6368
}
6469

@@ -658,8 +663,8 @@ func RunMigrationCommand(t *testing.T, v4Config string, tmpDir string) {
658663
cmd = exec.Command("go", "run", "-C", migratePath, ".",
659664
"-config", tmpDir,
660665
"-state", filepath.Join(stateDir, "terraform.tfstate"),
661-
"-grit=false", // Disable Grit transformations
662-
"-transformer=true", // Enable YAML transformations
666+
"-grit=false", // Disable Grit transformations
667+
"-transformer=true", // Enable YAML transformations
663668
"-transformer-dir", transformerDir) // Use local YAML configs
664669
cmd.Dir = tmpDir
665670
// Capture output for debugging

internal/services/workers_script/migrations.go

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,154 @@ package workers_script
55
import (
66
"context"
77

8+
"github.com/cloudflare/terraform-provider-cloudflare/internal/customfield"
9+
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
810
"github.com/hashicorp/terraform-plugin-framework/resource"
11+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
12+
"github.com/hashicorp/terraform-plugin-framework/types"
913
)
1014

1115
var _ resource.ResourceWithUpgradeState = (*WorkersScriptResource)(nil)
1216

1317
func (r *WorkersScriptResource) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
14-
return map[int64]resource.StateUpgrader{}
18+
return map[int64]resource.StateUpgrader{
19+
0: {
20+
PriorSchema: resourceSchemaV0(ctx),
21+
StateUpgrader: upgradeStateFromV0,
22+
},
23+
}
24+
}
25+
26+
// The schema is identical to the schema in schema.go, except the version is 0
27+
// and the assets.config.run_worker_first field is a boolean instead of a
28+
// dynamic value.
29+
func resourceSchemaV0(ctx context.Context) *schema.Schema {
30+
resourceSchemaLatest := ResourceSchema(ctx)
31+
resourceSchemaLatest.Version = 0
32+
resourceSchemaLatest.
33+
Attributes["assets"].(schema.SingleNestedAttribute).
34+
Attributes["config"].(schema.SingleNestedAttribute).
35+
Attributes["run_worker_first"] = schema.BoolAttribute{Optional: true}
36+
return &resourceSchemaLatest
37+
}
38+
39+
// State upgrade function from version 0 to version 1. This converts
40+
// assets.config.run_worker_first from a boolean value to a dynamic value
41+
// (either boolean or list of strings).
42+
func upgradeStateFromV0(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
43+
var priorStateData resourceModelV0
44+
resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...)
45+
if resp.Diagnostics.HasError() {
46+
return
47+
}
48+
49+
// Copy over all state data except for assets unchanged.
50+
newStateData := WorkersScriptModel{
51+
ID: priorStateData.ID,
52+
ScriptName: priorStateData.ScriptName,
53+
AccountID: priorStateData.AccountID,
54+
Content: priorStateData.Content,
55+
ContentFile: priorStateData.ContentFile,
56+
ContentSHA256: priorStateData.ContentSHA256,
57+
ContentType: priorStateData.ContentType,
58+
CreatedOn: priorStateData.CreatedOn,
59+
Etag: priorStateData.Etag,
60+
HasAssets: priorStateData.HasAssets,
61+
HasModules: priorStateData.HasModules,
62+
LastDeployedFrom: priorStateData.LastDeployedFrom,
63+
MigrationTag: priorStateData.MigrationTag,
64+
ModifiedOn: priorStateData.ModifiedOn,
65+
StartupTimeMs: priorStateData.StartupTimeMs,
66+
Handlers: priorStateData.Handlers,
67+
NamedHandlers: priorStateData.NamedHandlers,
68+
WorkersScriptMetadataModel: WorkersScriptMetadataModel{
69+
Bindings: priorStateData.Bindings,
70+
BodyPart: priorStateData.BodyPart,
71+
CompatibilityDate: priorStateData.CompatibilityDate,
72+
CompatibilityFlags: priorStateData.CompatibilityFlags,
73+
KeepAssets: priorStateData.KeepAssets,
74+
KeepBindings: priorStateData.KeepBindings,
75+
Limits: priorStateData.Limits,
76+
Logpush: priorStateData.Logpush,
77+
MainModule: priorStateData.MainModule,
78+
Migrations: priorStateData.Migrations,
79+
Observability: priorStateData.Observability,
80+
Placement: priorStateData.Placement,
81+
TailConsumers: priorStateData.TailConsumers,
82+
UsageModel: priorStateData.UsageModel,
83+
},
84+
}
85+
86+
if priorStateData.Assets != nil {
87+
newStateData.Assets = &WorkersScriptMetadataAssetsModel{
88+
JWT: priorStateData.Assets.JWT,
89+
Directory: priorStateData.Assets.Directory,
90+
AssetManifestSHA256: priorStateData.Assets.AssetManifestSHA256,
91+
}
92+
if priorStateData.Assets.Config != nil {
93+
newStateData.Assets.Config = &WorkersScriptMetadataAssetsConfigModel{
94+
Headers: priorStateData.Assets.Config.Headers,
95+
Redirects: priorStateData.Assets.Config.Redirects,
96+
HTMLHandling: priorStateData.Assets.Config.HTMLHandling,
97+
NotFoundHandling: priorStateData.Assets.Config.NotFoundHandling,
98+
ServeDirectly: priorStateData.Assets.Config.ServeDirectly,
99+
// Convert run_worker_first from boolean to dynamic value.
100+
RunWorkerFirst: customfield.RawNormalizedDynamicValueFrom(priorStateData.Assets.Config.RunWorkerFirst),
101+
}
102+
103+
}
104+
}
105+
106+
resp.Diagnostics.Append(resp.State.Set(ctx, newStateData)...)
107+
}
108+
109+
type resourceModelV0 struct {
110+
ID types.String `tfsdk:"id" json:"-,computed"`
111+
ScriptName types.String `tfsdk:"script_name" path:"script_name,required"`
112+
AccountID types.String `tfsdk:"account_id" path:"account_id,required"`
113+
Content types.String `tfsdk:"content" json:"-"`
114+
ContentFile types.String `tfsdk:"content_file" json:"-"`
115+
ContentSHA256 types.String `tfsdk:"content_sha256" json:"-"`
116+
ContentType types.String `tfsdk:"content_type" json:"-"`
117+
CreatedOn timetypes.RFC3339 `tfsdk:"created_on" json:"created_on,computed" format:"date-time"`
118+
Etag types.String `tfsdk:"etag" json:"etag,computed"`
119+
HasAssets types.Bool `tfsdk:"has_assets" json:"has_assets,computed"`
120+
HasModules types.Bool `tfsdk:"has_modules" json:"has_modules,computed"`
121+
LastDeployedFrom types.String `tfsdk:"last_deployed_from" json:"last_deployed_from,computed"`
122+
MigrationTag types.String `tfsdk:"migration_tag" json:"migration_tag,computed"`
123+
ModifiedOn timetypes.RFC3339 `tfsdk:"modified_on" json:"modified_on,computed" format:"date-time"`
124+
StartupTimeMs types.Int64 `tfsdk:"startup_time_ms" json:"startup_time_ms,computed"`
125+
Handlers customfield.List[types.String] `tfsdk:"handlers" json:"handlers,computed"`
126+
NamedHandlers customfield.NestedObjectList[WorkersScriptNamedHandlersModel] `tfsdk:"named_handlers" json:"named_handlers,computed"`
127+
128+
// Embedded metadata properties
129+
Bindings customfield.NestedObjectList[WorkersScriptMetadataBindingsModel] `tfsdk:"bindings" json:"bindings,computed_optional"`
130+
BodyPart types.String `tfsdk:"body_part" json:"body_part,optional"`
131+
CompatibilityDate types.String `tfsdk:"compatibility_date" json:"compatibility_date,computed_optional"`
132+
CompatibilityFlags customfield.Set[types.String] `tfsdk:"compatibility_flags" json:"compatibility_flags,computed_optional"`
133+
KeepAssets types.Bool `tfsdk:"keep_assets" json:"keep_assets,optional"`
134+
KeepBindings *[]types.String `tfsdk:"keep_bindings" json:"keep_bindings,optional"`
135+
Limits *WorkersScriptMetadataLimitsModel `tfsdk:"limits" json:"limits,optional"`
136+
Logpush types.Bool `tfsdk:"logpush" json:"logpush,computed_optional"`
137+
MainModule types.String `tfsdk:"main_module" json:"main_module,optional"`
138+
Migrations customfield.NestedObject[WorkersScriptMetadataMigrationsModel] `tfsdk:"migrations" json:"migrations,optional"`
139+
Observability *WorkersScriptMetadataObservabilityModel `tfsdk:"observability" json:"observability,optional"`
140+
Placement customfield.NestedObject[WorkersScriptMetadataPlacementModel] `tfsdk:"placement" json:"placement,computed_optional"`
141+
TailConsumers customfield.NestedObjectSet[WorkersScriptMetadataTailConsumersModel] `tfsdk:"tail_consumers" json:"tail_consumers,computed_optional"`
142+
UsageModel types.String `tfsdk:"usage_model" json:"usage_model,computed_optional"`
143+
144+
// Old assets type definition
145+
Assets *struct {
146+
Config *struct {
147+
Headers types.String `tfsdk:"headers" json:"_headers,optional"`
148+
Redirects types.String `tfsdk:"redirects" json:"_redirects,optional"`
149+
HTMLHandling types.String `tfsdk:"html_handling" json:"html_handling,optional"`
150+
NotFoundHandling types.String `tfsdk:"not_found_handling" json:"not_found_handling,optional"`
151+
RunWorkerFirst types.Bool `tfsdk:"run_worker_first" json:"run_worker_first,optional"`
152+
ServeDirectly types.Bool `tfsdk:"serve_directly" json:"serve_directly,optional"`
153+
} `tfsdk:"config" json:"config,optional"`
154+
JWT types.String `tfsdk:"jwt" json:"jwt,optional"`
155+
Directory types.String `tfsdk:"directory" json:"-,optional"`
156+
AssetManifestSHA256 types.String `tfsdk:"asset_manifest_sha256" json:"-,computed"`
157+
} `tfsdk:"assets" json:"assets,optional"`
15158
}

internal/services/workers_script/model.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,12 @@ type WorkersScriptMetadataAssetsModel struct {
122122
}
123123

124124
type WorkersScriptMetadataAssetsConfigModel struct {
125-
Headers types.String `tfsdk:"headers" json:"_headers,optional"`
126-
Redirects types.String `tfsdk:"redirects" json:"_redirects,optional"`
127-
HTMLHandling types.String `tfsdk:"html_handling" json:"html_handling,optional"`
128-
NotFoundHandling types.String `tfsdk:"not_found_handling" json:"not_found_handling,optional"`
129-
RunWorkerFirst types.Bool `tfsdk:"run_worker_first" json:"run_worker_first,optional"`
130-
ServeDirectly types.Bool `tfsdk:"serve_directly" json:"serve_directly,optional"`
125+
Headers types.String `tfsdk:"headers" json:"_headers,optional"`
126+
Redirects types.String `tfsdk:"redirects" json:"_redirects,optional"`
127+
HTMLHandling types.String `tfsdk:"html_handling" json:"html_handling,optional"`
128+
NotFoundHandling types.String `tfsdk:"not_found_handling" json:"not_found_handling,optional"`
129+
RunWorkerFirst customfield.NormalizedDynamicValue `tfsdk:"run_worker_first" json:"run_worker_first,optional"`
130+
ServeDirectly types.Bool `tfsdk:"serve_directly" json:"serve_directly,optional"`
131131
}
132132

133133
type WorkersScriptMetadataBindingsModel struct {

internal/services/workers_script/resource_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,155 @@ func TestAccCloudflareWorkerScript_ModuleWithDurableObject(t *testing.T) {
479479
})
480480
}
481481

482+
func TestAccCloudflareWorkerScript_AssetsConfigRunWorkerFirst(t *testing.T) {
483+
t.Parallel()
484+
485+
rnd := utils.GenerateRandomResourceName()
486+
name := "cloudflare_workers_script." + rnd
487+
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
488+
489+
contentDir := t.TempDir()
490+
contentFile := path.Join(contentDir, "index.js")
491+
writeContentFile := func(t *testing.T) {
492+
err := os.WriteFile(contentFile, []byte(`export default { fetch() { return new Response('Hello world'); } };`), 0644)
493+
if err != nil {
494+
t.Fatalf("Error creating temp file at path %s: %s", contentFile, err.Error())
495+
}
496+
}
497+
498+
assetsDir := t.TempDir()
499+
assetFile := path.Join(assetsDir, "index.html")
500+
writeAssetFile := func(t *testing.T) {
501+
err := os.WriteFile(assetFile, []byte("Hello world"), 0644)
502+
if err != nil {
503+
t.Fatalf("Error creating temp file at path %s: %s", assetFile, err.Error())
504+
}
505+
}
506+
507+
cleanup := func(t *testing.T) {
508+
for _, file := range []string{assetFile, contentFile} {
509+
err := os.Remove(file)
510+
if err != nil {
511+
t.Logf("Error removing temp file at path %s: %s", file, err.Error())
512+
}
513+
}
514+
}
515+
defer cleanup(t)
516+
517+
resource.Test(t, resource.TestCase{
518+
PreCheck: func() {
519+
acctest.TestAccPreCheck(t)
520+
acctest.TestAccPreCheck_AccountID(t)
521+
},
522+
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
523+
Steps: []resource.TestStep{
524+
{
525+
PreConfig: func() {
526+
writeContentFile(t)
527+
writeAssetFile(t)
528+
},
529+
Config: testAccCheckCloudflareWorkerScriptConfigWithAssetsWithRunWorkerFirst(rnd, accountID, contentFile, assetsDir, `false`),
530+
ConfigStateChecks: []statecheck.StateCheck{
531+
statecheck.ExpectKnownValue(name, tfjsonpath.New("assets").AtMapKey("config").AtMapKey("run_worker_first"), knownvalue.Bool(false)),
532+
},
533+
},
534+
{
535+
Config: testAccCheckCloudflareWorkerScriptConfigWithAssetsWithRunWorkerFirst(rnd, accountID, contentFile, assetsDir, `["/api/*"]`),
536+
ConfigStateChecks: []statecheck.StateCheck{
537+
statecheck.ExpectKnownValue(name, tfjsonpath.New("assets").AtMapKey("config").AtMapKey("run_worker_first"), knownvalue.ListExact([]knownvalue.Check{
538+
knownvalue.StringExact("/api/*"),
539+
})),
540+
},
541+
},
542+
{
543+
Config: testAccCheckCloudflareWorkerScriptConfigWithAssetsWithRunWorkerFirst(rnd, accountID, contentFile, assetsDir, `true`),
544+
ConfigStateChecks: []statecheck.StateCheck{
545+
statecheck.ExpectKnownValue(name, tfjsonpath.New("assets").AtMapKey("config").AtMapKey("run_worker_first"), knownvalue.Bool(true)),
546+
},
547+
},
548+
{
549+
Config: testAccCheckCloudflareWorkerScriptConfigWithAssetsWithRunWorkerFirst(rnd, accountID, contentFile, assetsDir, `["/api/*", "!/api/health"]`),
550+
ConfigStateChecks: []statecheck.StateCheck{
551+
statecheck.ExpectKnownValue(name, tfjsonpath.New("assets").AtMapKey("config").AtMapKey("run_worker_first"), knownvalue.ListExact([]knownvalue.Check{
552+
knownvalue.StringExact("/api/*"),
553+
knownvalue.StringExact("!/api/health"),
554+
})),
555+
},
556+
},
557+
},
558+
})
559+
}
560+
561+
func TestAccCloudflareWorkerScript_AssetsConfigRunWorkerFirstMigration(t *testing.T) {
562+
t.Parallel()
563+
564+
rnd := utils.GenerateRandomResourceName()
565+
name := "cloudflare_workers_script." + rnd
566+
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
567+
568+
contentDir := t.TempDir()
569+
contentFile := path.Join(contentDir, "index.js")
570+
writeContentFile := func(t *testing.T) {
571+
err := os.WriteFile(contentFile, []byte(`export default { fetch() { return new Response('Hello world'); } };`), 0644)
572+
if err != nil {
573+
t.Fatalf("Error creating temp file at path %s: %s", contentFile, err.Error())
574+
}
575+
}
576+
577+
assetsDir := t.TempDir()
578+
assetFile := path.Join(assetsDir, "index.html")
579+
writeAssetFile := func(t *testing.T) {
580+
err := os.WriteFile(assetFile, []byte("Hello world"), 0644)
581+
if err != nil {
582+
t.Fatalf("Error creating temp file at path %s: %s", assetFile, err.Error())
583+
}
584+
}
585+
586+
cleanup := func(t *testing.T) {
587+
for _, file := range []string{assetFile, contentFile} {
588+
err := os.Remove(file)
589+
if err != nil {
590+
t.Logf("Error removing temp file at path %s: %s", file, err.Error())
591+
}
592+
}
593+
}
594+
defer cleanup(t)
595+
596+
resource.Test(t, resource.TestCase{
597+
PreCheck: func() {
598+
acctest.TestAccPreCheck(t)
599+
acctest.TestAccPreCheck_AccountID(t)
600+
},
601+
Steps: []resource.TestStep{
602+
{
603+
PreConfig: func() {
604+
writeContentFile(t)
605+
writeAssetFile(t)
606+
},
607+
ExternalProviders: map[string]resource.ExternalProvider{
608+
"cloudflare": {
609+
Source: "cloudflare/cloudflare",
610+
VersionConstraint: "5.10.1",
611+
},
612+
},
613+
Config: testAccCheckCloudflareWorkerScriptConfigWithAssetsWithRunWorkerFirst(rnd, accountID, contentFile, assetsDir, `false`),
614+
ConfigStateChecks: []statecheck.StateCheck{
615+
statecheck.ExpectKnownValue(name, tfjsonpath.New("assets").AtMapKey("config").AtMapKey("run_worker_first"), knownvalue.Bool(false)),
616+
},
617+
},
618+
{
619+
Config: testAccCheckCloudflareWorkerScriptConfigWithAssetsWithRunWorkerFirst(rnd, accountID, contentFile, assetsDir, `["/api/*"]`),
620+
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
621+
ConfigStateChecks: []statecheck.StateCheck{
622+
statecheck.ExpectKnownValue(name, tfjsonpath.New("assets").AtMapKey("config").AtMapKey("run_worker_first"), knownvalue.ListExact([]knownvalue.Check{
623+
knownvalue.StringExact("/api/*"),
624+
})),
625+
},
626+
},
627+
},
628+
})
629+
}
630+
482631
func testAccCheckCloudflareWorkerScriptConfigServiceWorkerInitial(rnd, accountID string) string {
483632
return acctest.LoadTestCase("service_worker_initial.tf", rnd, scriptContent1, accountID)
484633
}
@@ -507,3 +656,7 @@ func testAccWorkersScriptConfigWithInvalidContentSHA256(rnd, accountID, contentF
507656
func testAccWorkersScriptConfigWithAssets(rnd, accountID, assetsDir string) string {
508657
return acctest.LoadTestCase("module_with_assets.tf", rnd, accountID, assetsDir)
509658
}
659+
660+
func testAccCheckCloudflareWorkerScriptConfigWithAssetsWithRunWorkerFirst(rnd, accountID, contentFile, assetsDir, runWorkerFirst string) string {
661+
return acctest.LoadTestCase("module_with_assets_with_run_worker_first.tf", rnd, accountID, contentFile, assetsDir, runWorkerFirst)
662+
}

0 commit comments

Comments
 (0)