Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions e2e/drift-exemptions/workers_secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Drift Exemptions for workers_secret resource
#
# v4 cloudflare_workers_secret / cloudflare_worker_secret resources are folded
# into cloudflare_workers_script bindings in v5. The secrets were managed via
# the Workers Secrets API in v4 but become secret_text bindings on the parent
# script in v5. This causes expected drift on the first plan:
#
# - New secret_text bindings appear as additions to the bindings list
# - Existing bindings may reorder (concat merges secret bindings at the end)
#
# Stabilizes after first apply.

version: 1

exemptions:
- name: "workers_secret_folded_into_bindings"
description: "Worker secrets are folded into workers_script bindings in v5. The first plan will show binding additions and reordering. This is expected and resolves after terraform apply."
resource_types:
- "cloudflare_workers_script"
resource_name_patterns:
- "module.workers_secret.*"
patterns:
- 'type.*=.*"secret_text"'
- 'text.*=.*\(sensitive value\)'
- 'name.*=.*"API_KEY"'
- 'name.*=.*"DB_PASSWORD"'
- 'name.*=.*"JWT_SECRET"'
- 'name.*=.*"SINGULAR_SECRET"'
- 'name.*=.*"LITERAL_SECRET"'
- 'name.*=.*"EXTRA_SECRET"'
- 'name.*=.*"MY_KV"'
- 'name.*=.*"ENV"'
- 'namespace_id'
- 'type.*=.*"kv_namespace"'
- 'type.*=.*"plain_text"'
enabled: true

settings:
apply_exemptions: true
verbose_exemptions: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
variable "cloudflare_account_id" {
description = "Cloudflare account ID"
type = string
}

variable "cloudflare_zone_id" {
description = "Cloudflare zone ID"
type = string
}

variable "cloudflare_domain" {
description = "Cloudflare domain for testing"
type = string
}

# ========================================
# Pattern 1: Single secret with parent script in same file
# ========================================

resource "cloudflare_workers_script" "basic_worker" {
account_id = var.cloudflare_account_id
content = "addEventListener('fetch', event => { event.respondWith(new Response('Hello')); });"
script_name = "cftftest-basic-worker"
bindings = [
{
type = "secret_text"
name = "API_KEY"
text = "my-api-key-value"
}
]
}


# ========================================
# Pattern 2: Multiple secrets for the same script
# ========================================

resource "cloudflare_workers_script" "multi_secret_worker" {
account_id = var.cloudflare_account_id
content = "addEventListener('fetch', event => { event.respondWith(new Response('Multi')); });"
script_name = "cftftest-multi-secret-worker"
bindings = [
{
type = "secret_text"
name = "DB_PASSWORD"
text = "super-secret-db-password"
}, {
type = "secret_text"
name = "JWT_SECRET"
text = "jwt-signing-key"
}
]
}



# ========================================
# Pattern 3: Deprecated singular form (cloudflare_worker_secret)
# ========================================



# ========================================
# Pattern 4: Secret with script that has existing bindings
# ========================================

resource "cloudflare_workers_kv_namespace" "test_kv" {
account_id = var.cloudflare_account_id
title = "cftftest-kv-for-secret-test"
}

resource "cloudflare_workers_script" "worker_with_bindings" {
account_id = var.cloudflare_account_id
content = "addEventListener('fetch', event => { event.respondWith(new Response('Bindings')); });"


script_name = "cftftest-worker-with-bindings"
bindings = concat([
{
type = "kv_namespace"
name = "MY_KV"
namespace_id = cloudflare_workers_kv_namespace.test_kv.id
}, {
type = "plain_text"
name = "ENV"
text = "production"
}
], [
{
type = "secret_text"
name = "EXTRA_SECRET"
text = "extra-secret-value"
}
])
}


# ========================================
# Pattern 5: Secret referencing script via depends_on (literal name)
# ========================================

resource "cloudflare_workers_script" "literal_match_worker" {
account_id = var.cloudflare_account_id
content = "addEventListener('fetch', event => { event.respondWith(new Response('Literal')); });"
script_name = "cftftest-literal-match"
bindings = [
{
type = "secret_text"
name = "LITERAL_SECRET"
text = "literal-secret-value"
}
]
}


removed {
from = cloudflare_workers_secret.basic_secret
lifecycle {
destroy = false
}
}

removed {
from = cloudflare_workers_secret.db_password
lifecycle {
destroy = false
}
}

removed {
from = cloudflare_workers_secret.jwt_secret
lifecycle {
destroy = false
}
}

resource "cloudflare_workers_script" "singular_worker" {
account_id = var.cloudflare_account_id
content = "addEventListener('fetch', event => { event.respondWith(new Response('Singular')); });"
script_name = "cftftest-singular-worker"
bindings = [
{
type = "secret_text"
name = "SINGULAR_SECRET"
text = "singular-secret-value"
}
]
}

moved {
from = cloudflare_worker_script.singular_worker
to = cloudflare_workers_script.singular_worker
}

removed {
from = cloudflare_worker_secret.singular_secret
lifecycle {
destroy = false
}
}

removed {
from = cloudflare_workers_secret.binding_secret
lifecycle {
destroy = false
}
}

removed {
from = cloudflare_workers_secret.literal_match_secret
lifecycle {
destroy = false
}
}
122 changes: 122 additions & 0 deletions integration/v4_to_v5/testdata/workers_secret/input/workers_secret.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
variable "cloudflare_account_id" {
description = "Cloudflare account ID"
type = string
}

variable "cloudflare_zone_id" {
description = "Cloudflare zone ID"
type = string
}

variable "cloudflare_domain" {
description = "Cloudflare domain for testing"
type = string
}

# ========================================
# Pattern 1: Single secret with parent script in same file
# ========================================

resource "cloudflare_workers_script" "basic_worker" {
account_id = var.cloudflare_account_id
name = "cftftest-basic-worker"
content = "addEventListener('fetch', event => { event.respondWith(new Response('Hello')); });"
}

resource "cloudflare_workers_secret" "basic_secret" {
account_id = var.cloudflare_account_id
script_name = cloudflare_workers_script.basic_worker.name
name = "API_KEY"
secret_text = "my-api-key-value"
}

# ========================================
# Pattern 2: Multiple secrets for the same script
# ========================================

resource "cloudflare_workers_script" "multi_secret_worker" {
account_id = var.cloudflare_account_id
name = "cftftest-multi-secret-worker"
content = "addEventListener('fetch', event => { event.respondWith(new Response('Multi')); });"
}

resource "cloudflare_workers_secret" "db_password" {
account_id = var.cloudflare_account_id
script_name = cloudflare_workers_script.multi_secret_worker.name
name = "DB_PASSWORD"
secret_text = "super-secret-db-password"
}

resource "cloudflare_workers_secret" "jwt_secret" {
account_id = var.cloudflare_account_id
script_name = cloudflare_workers_script.multi_secret_worker.name
name = "JWT_SECRET"
secret_text = "jwt-signing-key"
}

# ========================================
# Pattern 3: Deprecated singular form (cloudflare_worker_secret)
# ========================================

resource "cloudflare_worker_script" "singular_worker" {
account_id = var.cloudflare_account_id
name = "cftftest-singular-worker"
content = "addEventListener('fetch', event => { event.respondWith(new Response('Singular')); });"
}

resource "cloudflare_worker_secret" "singular_secret" {
account_id = var.cloudflare_account_id
script_name = cloudflare_worker_script.singular_worker.name
name = "SINGULAR_SECRET"
secret_text = "singular-secret-value"
}

# ========================================
# Pattern 4: Secret with script that has existing bindings
# ========================================

resource "cloudflare_workers_kv_namespace" "test_kv" {
account_id = var.cloudflare_account_id
title = "cftftest-kv-for-secret-test"
}

resource "cloudflare_workers_script" "worker_with_bindings" {
account_id = var.cloudflare_account_id
name = "cftftest-worker-with-bindings"
content = "addEventListener('fetch', event => { event.respondWith(new Response('Bindings')); });"

kv_namespace_binding {
name = "MY_KV"
namespace_id = cloudflare_workers_kv_namespace.test_kv.id
}

plain_text_binding {
name = "ENV"
text = "production"
}
}

resource "cloudflare_workers_secret" "binding_secret" {
account_id = var.cloudflare_account_id
script_name = cloudflare_workers_script.worker_with_bindings.name
name = "EXTRA_SECRET"
secret_text = "extra-secret-value"
}

# ========================================
# Pattern 5: Secret referencing script via depends_on (literal name)
# ========================================

resource "cloudflare_workers_script" "literal_match_worker" {
account_id = var.cloudflare_account_id
name = "cftftest-literal-match"
content = "addEventListener('fetch', event => { event.respondWith(new Response('Literal')); });"
}

resource "cloudflare_workers_secret" "literal_match_secret" {
account_id = var.cloudflare_account_id
script_name = "cftftest-literal-match"
name = "LITERAL_SECRET"
secret_text = "literal-secret-value"
depends_on = [cloudflare_workers_script.literal_match_worker]
}
8 changes: 6 additions & 2 deletions internal/e2e-runner/drift.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,13 @@ func loadResourceExemptions(repoRoot string, resource string) (*DriftExemptionsC
// Validate resource_type if specified
if config.Version >= 1 {
expectedType := "cloudflare_" + resource
// Check if any exemption has a different resource type restriction
// Check if any exemption has a different resource type restriction.
// Skip the warning when the exemption also has resource_name_patterns,
// because that means the author is intentionally targeting a different
// resource type (e.g. workers_secret.yaml targets cloudflare_workers_script
// since secrets are folded into script bindings in v5).
for _, e := range config.Exemptions {
if len(e.ResourceTypes) > 0 {
if len(e.ResourceTypes) > 0 && len(e.ResourceNamePatterns) == 0 {
found := false
for _, rt := range e.ResourceTypes {
if rt == expectedType {
Expand Down
2 changes: 2 additions & 0 deletions internal/e2e-runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ func RunE2ETests(cfg *RunConfig) error {
"cloudflare_access_policy": true, // Application-scoped policies with application_id cannot be migrated; removed{} blocks handle state cleanup
"cloudflare_split_tunnel": true, // Dissolved into device profile exclude/include attributes in v5
"cloudflare_zero_trust_split_tunnel": true, // Newer v4 name for split_tunnel — also dissolved in v5
"cloudflare_workers_secret": true, // Folded into workers_script bindings in v5
"cloudflare_worker_secret": true, // Deprecated singular form — also folded into workers_script bindings
}
stateFilePath := filepath.Join(v5Dir, "terraform.tfstate")
if removed, err := removeObsoleteStateEntries(stateFilePath, obsoleteTypes); err != nil {
Expand Down
2 changes: 2 additions & 0 deletions internal/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import (
"github.com/cloudflare/tf-migrate/internal/resources/workers_kv"
"github.com/cloudflare/tf-migrate/internal/resources/workers_kv_namespace"
"github.com/cloudflare/tf-migrate/internal/resources/workers_script"
"github.com/cloudflare/tf-migrate/internal/resources/workers_secret"
"github.com/cloudflare/tf-migrate/internal/resources/zero_trust_access_application"
"github.com/cloudflare/tf-migrate/internal/resources/zero_trust_access_group"
"github.com/cloudflare/tf-migrate/internal/resources/zero_trust_access_identity_provider"
Expand Down Expand Up @@ -164,6 +165,7 @@ func RegisterAllMigrations() {
workers_kv_namespace.NewV4ToV5Migrator()
workers_script.NewV4ToV5Migrator()
workers_for_platforms_dispatch_namespace.NewV4ToV5Migrator()
workers_secret.NewV4ToV5Migrator()
zero_trust_access_application.NewV4ToV5Migrator()
zero_trust_access_group.NewV4ToV5Migrator()
zero_trust_access_identity_provider.NewV4ToV5Migrator()
Expand Down
Loading
Loading