Skip to content

MIgrate to Cloudflare R2 and add email support#16

Merged
clofour merged 5 commits intomainfrom
dev
Apr 25, 2026
Merged

MIgrate to Cloudflare R2 and add email support#16
clofour merged 5 commits intomainfrom
dev

Conversation

@clofour
Copy link
Copy Markdown
Owner

@clofour clofour commented Apr 25, 2026

Summary by CodeRabbit

Release Notes

  • Chores
    • Migrated object storage from DigitalOcean Spaces to Cloudflare R2
    • Integrated SendGrid as the email delivery provider with SMTP configuration
    • Updated email settings to support domain-based sender and reply-to addresses

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e0bbdcc8-6b14-46a1-90bb-c7173cd0e10f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
terraform/helm.tf (1)

108-125: ⚠️ Potential issue | 🟠 Major

Add kubernetes_secret_v1.gitlab_sendgrid_secret to depends_on.

The Helm values reference the gitlab-sendgrid-secret for SMTP password, but it isn't listed in this depends_on. On a clean apply, Terraform may schedule the Helm release before the secret exists, causing pod startup failures or a transient reconcile error.

🔧 Proposed fix
         kubernetes_secret_v1.gitlab_redis,
-        kubernetes_secret_v1.gitlab_s3_main
+        kubernetes_secret_v1.gitlab_s3_main,
+        kubernetes_secret_v1.gitlab_sendgrid_secret
     ]
terraform/outputs.tf (1)

44-50: ⚠️ Potential issue | 🟠 Major

Stale output naming and endpoint after R2 migration.

  • output "spaces_endpoint" still emits a DigitalOcean Spaces URL, but the buckets it logically pairs with are now Cloudflare R2. Any consumer using both will end up pointing at the wrong endpoint. Update to the R2 S3-compatible endpoint (e.g., https://${var.cloudflare_account_id}.r2.cloudflarestorage.com or the EU jurisdiction variant).
  • output "spaces_buckets" should be renamed (e.g., r2_buckets) to reflect the new backing resource and avoid confusing downstream consumers.
terraform/kubernetes.tf (1)

103-115: ⚠️ Potential issue | 🔴 Critical

Critical: GitLab S3 connection is broken — wrong endpoint and wrong credentials for R2.

This secret will not authenticate against Cloudflare R2:

  1. endpoint still points at DigitalOcean Spaces (https://${var.region}.digitaloceanspaces.com). For R2 it must be https://<account_id>.r2.cloudflarestorage.com (or jurisdiction-specific variant such as .eu.r2.cloudflarestorage.com).
  2. aws_access_key_id/aws_secret_access_key must be an R2 Access Key ID and Secret Access Key generated via the R2 dashboard/API. var.cloudflare_account_id is an account identifier (not a key) and var.cloudflare_api_token is a Cloudflare API token used for the Terraform provider — not the R2 S3 secret. R2's S3-compatible API uses SigV4 with R2-specific keys.
  3. For R2, region should be auto.

The GitHub Actions workflow already injects R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY as AWS_* env vars (for the TF backend). Plumb those through as new TF_VAR_r2_access_key_id / TF_VAR_r2_secret_access_key environment variables and use them here. Apply the same fix to gitlab_s3_backup (lines 136–154).

Proposed fix for gitlab_s3_main
     data = {
         connection = yamlencode({
             provider = "AWS"
-            region = var.region
-            endpoint = "https://${var.region}.digitaloceanspaces.com"
-            aws_access_key_id = var.cloudflare_account_id
-            aws_secret_access_key = var.cloudflare_api_token
+            region = "auto"
+            endpoint = "https://${var.cloudflare_account_id}.r2.cloudflarestorage.com"
+            aws_access_key_id = var.r2_access_key_id
+            aws_secret_access_key = var.r2_secret_access_key
             path_style = true
         })
     }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 41670b00-09f1-4557-9d22-8d275b82c86d

📥 Commits

Reviewing files that changed from the base of the PR and between 43cf3f5 and 978db85.

📒 Files selected for processing (11)
  • .github/workflows/terraform.yaml
  • .gitignore
  • helm/gitlab/values.yaml
  • terraform/helm.tf
  • terraform/kubernetes.tf
  • terraform/outputs.tf
  • terraform/providers.tf
  • terraform/s3.tf
  • terraform/spaces.tf
  • terraform/variables.tf
  • terraform/versions.tf
💤 Files with no reviewable changes (1)
  • terraform/spaces.tf
📜 Review details
🧰 Additional context used
🪛 Checkov (3.2.524)
helm/gitlab/values.yaml

[low] 89-90: Base64 High Entropy String

(CKV_SECRET_6)

🔇 Additional comments (6)
.gitignore (1)

2-2: Looks good — ignore rule is valid.

Adding TODO.md to .gitignore is straightforward and safe.

terraform/kubernetes.tf (2)

142-151: Same issue applies to gitlab_s3_backup.

Mirror the fix from gitlab_s3_main: switch the endpoint, region, and access key variables to R2 values. Otherwise GitLab backups silently fail to upload.


156-167: Sendgrid secret wiring looks correct.

Matches helm/gitlab/values.yaml SMTP config (secret: gitlab-sendgrid-secret, key: password).

terraform/providers.tf (1)

5-7: LGTM.

Cloudflare provider initialized with the API token variable; matches versions.tf and variables.tf.

terraform/versions.tf (1)

9-12: No action required—attributes are correct for Cloudflare provider 5.19.0.

The cloudflare_r2_bucket resource in terraform/s3.tf correctly uses account_id, name, and jurisdiction attributes for provider version ~> 5.19.0. All three attributes are valid in this version, with account_id and name as required fields and jurisdiction as optional.

helm/gitlab/values.yaml (1)

77-93: No issues found. Configuration keys for global.email and global.smtp are correct per GitLab Helm chart 9.10.3 documentation, including display_name, from, reply_to, and user_name in snake_case. The SendGrid SMTP settings with STARTTLS on port 587 are properly configured.

Comment thread .github/workflows/terraform.yaml
Comment thread terraform/s3.tf
Comment on lines +9 to +17
resource "random_id" "suffix" {
byte_length = 3
keepers = {
cluster_name = var.cluster_name
}
lifecycle {
prevent_destroy = true
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

keepers + prevent_destroy on random_id is contradictory.

If var.cluster_name ever changes, keepers will mark this resource for replacement, but prevent_destroy = true will then abort the plan with an error. If the goal is to lock the suffix permanently, drop keepers. If the goal is to rotate it with cluster name changes, drop prevent_destroy.

Comment thread terraform/s3.tf
Comment on lines +19 to +24
resource "cloudflare_r2_bucket" "gitlab" {
for_each = local.buckets
account_id = var.cloudflare_account_id
name = "${var.cluster_name}-${each.key}-${random_id.suffix.hex}"
jurisdiction = var.r2_jurisdiction
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bucket destroy/rename risk and naming-length concerns.

  1. cloudflare_r2_bucket.gitlab lacks lifecycle { prevent_destroy = true } even though random_id.suffix has it. Any change to var.cluster_name, each.key, or var.r2_jurisdiction recreates the bucket — R2 cannot rename, so this means destroy + recreate and data loss. Add prevent_destroy here too, given these buckets back GitLab artifacts/registry/backups.
  2. R2 bucket names must be 3–63 chars, lowercase alphanumerics + hyphens. With keys like ci-secure-files (15) and dependency-proxy (16) plus 6 hex chars and two dashes, cluster_name is effectively capped at ~40 chars. Consider validating cluster_name length, or hashing instead of concatenating.
🔧 Proposed fix
 resource "cloudflare_r2_bucket" "gitlab" {
     for_each = local.buckets
     account_id = var.cloudflare_account_id
     name = "${var.cluster_name}-${each.key}-${random_id.suffix.hex}"
     jurisdiction = var.r2_jurisdiction
+
+    lifecycle {
+        prevent_destroy = true
+    }
 }

Comment thread terraform/variables.tf
Comment on lines +32 to +35
variable "r2_jurisdiction" {
type = string
default = "eu"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🌐 Web query:

Cloudflare R2 supported jurisdictions list (default, eu, fedramp)

💡 Result:

Cloudflare R2 supports three jurisdictions for data storage: default (no restriction, data stored globally), eu (European Union), and fedramp (FedRAMP, requires Enterprise customers).

Citations:


Add validation block to constrain r2_jurisdiction to supported values.

Cloudflare R2 only accepts three jurisdictions: default, eu, and fedramp. Without a validation block, an invalid value will only fail at terraform apply time with an API error. Adding validation catches misconfiguration earlier.

♻️ Proposed validation
 variable "r2_jurisdiction" {
     type = string
     default = "eu"
+    validation {
+      condition     = contains(["default", "eu", "fedramp"], var.r2_jurisdiction)
+      error_message = "r2_jurisdiction must be one of: default, eu, fedramp."
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
variable "r2_jurisdiction" {
type = string
default = "eu"
}
variable "r2_jurisdiction" {
type = string
default = "eu"
validation {
condition = contains(["default", "eu", "fedramp"], var.r2_jurisdiction)
error_message = "r2_jurisdiction must be one of: default, eu, fedramp."
}
}

@clofour
Copy link
Copy Markdown
Owner Author

clofour commented Apr 25, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

✅ Actions performed

Full review triggered.

@clofour clofour merged commit 433b8c0 into main Apr 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant