refactor(tf): move freshdesk ownership into partition-level global/freshdesk stacks (#580)

This commit is contained in:
Antoine Lecompte
2026-08-27 14:35:04 -04:00
committed by GitHub
parent 64371fbaa9
commit fa9cfd3891
26 changed files with 692 additions and 189 deletions
+7 -4
View File
@@ -188,10 +188,13 @@ self-heals by re-reading messages after the stored cursors. Dormant unless
**Freshdesk-side setup**: the ticket-update **automation rule** (reply / **Freshdesk-side setup**: the ticket-update **automation rule** (reply /
public note / status change, performed by agent → webhook to the capability public note / status change, performed by agent → webhook to the capability
URL with the `x-freshdesk-secret` header) and the per-env **agent group** are URL with the `x-freshdesk-secret` header), the per-env **agent group**, and
**TF-managed** (`freshdesk.tf` in each talos stack, `slop-place/freshdesk` the webhook credentials themselves are owned by the partition-wide
provider); the rule's events/actions JSON is validated by Freshdesk itself on **`tf/deployment/<partition>/global/freshdesk` stack** (`slop-place/freshdesk`
first apply. What stays provider); the talos stack consumes the minted values back through 1P
(`YUCCA_FRESHDESK_WEBHOOK_{PATH,SECRET}`, `YUCCA_FRESHDESK_GROUP_ID`) into
the bot Secret and cluster-secrets. The rule's events/actions JSON is
validated by Freshdesk itself on first apply. What stays
manual, once per account: create a dedicated **bot agent** and put its API manual, once per account: create a dedicated **bot agent** and put its API
key in the `YUCCA_FRESHDESK_API_KEY` item (with `YUCCA_FRESHDESK_URL` beside key in the `YUCCA_FRESHDESK_API_KEY` item (with `YUCCA_FRESHDESK_URL` beside
it), and put an **admin** agent's key in `YUCCA_FRESHDESK_ADMIN_API_KEY` — it), and put an **admin** agent's key in `YUCCA_FRESHDESK_ADMIN_API_KEY` —
+4
View File
@@ -105,3 +105,7 @@ export TF_VAR_sietch_transcripts_secret_key="op://yucca_tf_staging/SIETCH_CEPH_S
export TF_VAR_yucca_freshdesk_url="op://yucca_tf_staging/YUCCA_FRESHDESK_URL/password" export TF_VAR_yucca_freshdesk_url="op://yucca_tf_staging/YUCCA_FRESHDESK_URL/password"
export TF_VAR_yucca_freshdesk_api_key="op://yucca_tf_staging/YUCCA_FRESHDESK_API_KEY/password" export TF_VAR_yucca_freshdesk_api_key="op://yucca_tf_staging/YUCCA_FRESHDESK_API_KEY/password"
export TF_VAR_yucca_freshdesk_admin_api_key="op://yucca_tf_staging/YUCCA_FRESHDESK_ADMIN_API_KEY/password" export TF_VAR_yucca_freshdesk_admin_api_key="op://yucca_tf_staging/YUCCA_FRESHDESK_ADMIN_API_KEY/password"
# Minted by the staging/global/freshdesk stack — uncomment after its first apply.
# export TF_VAR_yucca_freshdesk_webhook_secret="op://yucca_tf_staging/YUCCA_FRESHDESK_WEBHOOK_SECRET/password"
# export TF_VAR_yucca_freshdesk_webhook_path="op://yucca_tf_staging/YUCCA_FRESHDESK_WEBHOOK_PATH/password"
# export TF_VAR_yucca_freshdesk_group_id="op://yucca_tf_staging/YUCCA_FRESHDESK_GROUP_ID/password"
+4
View File
@@ -105,3 +105,7 @@ export TF_VAR_vmauth_remote_write_password="op://shared_tf_prod/O11Y_VICTORIAMET
export TF_VAR_yucca_freshdesk_url="op://yucca_tf_prod/YUCCA_FRESHDESK_URL/password" export TF_VAR_yucca_freshdesk_url="op://yucca_tf_prod/YUCCA_FRESHDESK_URL/password"
export TF_VAR_yucca_freshdesk_api_key="op://yucca_tf_prod/YUCCA_FRESHDESK_API_KEY/password" export TF_VAR_yucca_freshdesk_api_key="op://yucca_tf_prod/YUCCA_FRESHDESK_API_KEY/password"
export TF_VAR_yucca_freshdesk_admin_api_key="op://yucca_tf_prod/YUCCA_FRESHDESK_ADMIN_API_KEY/password" export TF_VAR_yucca_freshdesk_admin_api_key="op://yucca_tf_prod/YUCCA_FRESHDESK_ADMIN_API_KEY/password"
# Minted by the prod/global/freshdesk stack — uncomment after its first apply.
# export TF_VAR_yucca_freshdesk_webhook_secret="op://yucca_tf_prod/YUCCA_FRESHDESK_WEBHOOK_SECRET/password"
# export TF_VAR_yucca_freshdesk_webhook_path="op://yucca_tf_prod/YUCCA_FRESHDESK_WEBHOOK_PATH/password"
# export TF_VAR_yucca_freshdesk_group_id="op://yucca_tf_prod/YUCCA_FRESHDESK_GROUP_ID/password"
+69
View File
@@ -0,0 +1,69 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/1password/onepassword" {
version = "2.2.1"
constraints = "~> 2.1"
hashes = [
"h1:aH5pZUimlQdiGnDLHRC49W4xnkx52wfd8XRLt69+764=",
"zh:025709a6b5f1b3685d277f2c48f7cb8b53d14b3699c1123d7e9a2135c099c533",
"zh:037fc89d150063a8aacdcab08ba26038b489fe2468d509b842d298ea59096ca6",
"zh:233777182b25faf1658e8ce171b684460983bb41cff79fb243662f3f9dc5ca6c",
"zh:2fb5ca2fc8c37b1d1c54da646ed13bf40897941fe92eece784fba496f677b533",
"zh:4b25b5ce1f694ec265e65234fc85d6bdf3810297ffeaced54ad46a1ba28142de",
"zh:5509d1e4fb7b45c63124ec66fd1f9d6757daa8bf1f7bdd724d5adb2965b61436",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:a23ba946c629ec912b2fcbf606a2eb8853626ec0e0bee749f2d39146a872c082",
"zh:a3d3024485426237d7b4a4350b12dda4d29d88f3942246a9370be35ec2a51e9e",
"zh:a6ef65544ab8fc26d468b38636407a3d2d902e35c51b648729bf97c31d1937f9",
"zh:afbe9480a0da0ad8dc514b277f1e4be36b8931f045021d05c21665ef1ac0b7c8",
"zh:b2e96e69fa9ff7e179dccdef5b785cd020eb46bb2b3d1d507d009d71be6b0c26",
"zh:ceefaede9e8a3104463523ba267e3e985b27a706f7628a9ddd37330c2ca59d4d",
"zh:ea77786bd6809ff4f8043b84a0212fec4de18b7d51bc420417ba10999ca99887",
"zh:f7d8160c3669c8ab76a2da14ea740d91a08ca23d1fb657669e52a840b2b113d9",
]
}
provider "registry.opentofu.org/hashicorp/random" {
version = "3.9.0"
constraints = "~> 3.6"
hashes = [
"h1:U8KXqGCoNI9/guYbTvzgdtVk3fRthoG0UXwm1JoEpIs=",
"zh:03f1114cc20b8913523735ab76e0f0a2b16ce13c92923a53304bf85f07fc0dbc",
"zh:105b678ee72322a3067f105d7e05e940f6143238f377f6e87ff4ec909246ac2a",
"zh:55f3bbf13ea18cbace61a706566a80f25f33fe2b1780b6f3d7b582af2a05b6d2",
"zh:63adf996db48f082f7a6351eb485e219cd88795fc71e6ec60a837263ab0d2cb1",
"zh:7e99550738a4e3cc68b8a467714b0d69371025fe95e3326d5323d026d55653e9",
"zh:8342b54af3a18a37e075eeae61be57f4de2ba71b35d95c5075d402dd2c1f289d",
"zh:83ee18e32ac9dd5fc91298554b7c4cfa4c3a1db50f4c797945637cc93c0844ae",
"zh:993ecc0adbf6bd535a59fbc9b735d8c33950e6f6eb5e621d750da9b71d65d80a",
"zh:ad722bc59d4edbf1415e827fc007c0efe6e0e9462d5568bae20b34be1058a261",
"zh:ae9448e1f87b2f9a6c5197a0e9862162ec6b137cb3a3835e11522995d8939e7c",
"zh:bc9cdd3aac784f759125c6627f6f6416e8726a1c184eb9cf3e55b9edbc94c627",
"zh:c8e35b89572ba1c40a9b20022e033a3395fb8d42e7604d50c900f193ba10382e",
"zh:e2deaa8a9975ef81d9f62baed12c41286918b0a10908e0e031f13f69a3b730a1",
"zh:ee39707557210a0ab1098aa357d2cdfe502e5a312d0dbdffb09d08facc4d3fc5",
"zh:f81afe4eb63e8aa9e0ea71be6c990f0dc69cb360e7191c0742a991f4a5081b64",
]
}
provider "registry.terraform.io/slop-place/freshdesk" {
version = "0.1.1"
constraints = "~> 0.1"
hashes = [
"h1:VyOKR4IfX4LWwW1Gv6rAa5A5x/h0IEDqKHL4hjliao8=",
"zh:19326bde07045235a0b41a1e0936a0929ee5b8a63d1856199dde34c1e2a2f641",
"zh:2221a63af314b9fae115bd455564e80105ead873856ebff4a9ae967fa91b9408",
"zh:239a2dc1433199705b587470fc86fc5d75761e2b6b2d640a112d416966676479",
"zh:4b7678d13a51cf6525ea7aec5788e2fcb96c8ef6b9f853937d224946f928ddb4",
"zh:74e33bffa31c664a3ae1cb20d098c804ff92c83fc042ec7d24b68f0ebd8f027f",
"zh:7943bf339e8f825b56c109ad92702102cdf4d07d577c48e45228e2f0d7889eaf",
"zh:7d4958ed366239294b085542859d5cc135b8041e1c5c5ab2a0b8c278c45df665",
"zh:91b8bcc4e58ba08f4e3380d07d6df32719d8464607cc1a0c56f75978cccd0cc9",
"zh:9deb669d3cd5dd598f83d51112987540c71c46f49b6d7841fd72a88fcb84f3f0",
"zh:ae3a04d72429f1b45db0cb1b1741ff007fc558c864c3b64652a48f1ab636f102",
"zh:d477d2919ff96f3d58be4df678f8eca52fb24254d53590fddca6b34d49d48215",
"zh:dd743e45b051dcff9b4f88bd99a6c5c24be9a614ee750625f900b4d0022634e8",
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
]
}
@@ -0,0 +1,31 @@
# ── Discovery contract ──────────────────────────────────────────────────────
# Single non-sensitive envelope consumed by yuctl. The credential values stay
# in 1P — only the item titles are output. See tf/README.md.
output "discovery_schema_version" {
description = "Schema version of the discovery output envelope."
value = 1
}
output "discovery" {
description = "Freshdesk payload for this partition (non-sensitive)."
value = {
schema_version = 1
partition = var.partition
region = var.region
slug = var.slug
role = var.role
stack = var.stack
stack_type = "freshdesk"
freshdesk = {
vault = "yucca_tf_${var.partition}"
group_name = "FUTO Cloud"
rule_managed = nonsensitive(local.enabled)
item_titles = {
webhook_path = "YUCCA_FRESHDESK_WEBHOOK_PATH"
webhook_secret = "YUCCA_FRESHDESK_WEBHOOK_SECRET"
group_id = "YUCCA_FRESHDESK_GROUP_ID"
}
}
}
}
@@ -0,0 +1,99 @@
# The webhook contract, TF-owned end to end: this stack mints the capability-
# URL path segment and the x-freshdesk-secret header, mirrors them into 1P,
# and points the Freshdesk automation rule at them — no human ever handles
# the values, and the talos stack consumes them back via op:// refs (bot
# Secret + cluster-secrets). See docs/discord-support.md.
locals {
# yucca-manual-secrets placeholders read back literally as REPLACE_ME —
# never let that masquerade as config.
url = var.yucca_freshdesk_url == "REPLACE_ME" ? "" : var.yucca_freshdesk_url
admin_api_key = var.yucca_freshdesk_admin_api_key == "REPLACE_ME" ? "" : var.yucca_freshdesk_admin_api_key
enabled = local.url != "" && local.admin_api_key != ""
}
# Fallbacks keep the provider configurable while the manual items are
# unfilled — never contacted, the freshdesk resources are count-gated.
provider "freshdesk" {
domain = coalesce(local.url, "https://freshdesk.invalid")
api_key = coalesce(local.admin_api_key, "unset")
}
provider "onepassword" {}
data "onepassword_vault" "partition" {
name = "yucca_tf_${var.partition}"
}
# Generated unconditionally (no Freshdesk dependency) so the talos stack's
# op:// refs can be uncommented right after this stack's first apply.
resource "random_password" "webhook_secret" {
length = 48
special = false
}
resource "onepassword_item" "webhook_secret" {
vault = data.onepassword_vault.partition.uuid
title = "YUCCA_FRESHDESK_WEBHOOK_SECRET"
category = "password"
password = random_password.webhook_secret.result
}
resource "random_password" "webhook_path" {
length = 32
special = false
}
resource "onepassword_item" "webhook_path" {
vault = data.onepassword_vault.partition.uuid
title = "YUCCA_FRESHDESK_WEBHOOK_PATH"
category = "password"
password = random_password.webhook_path.result
}
# Bot-created tickets land here; the id reaches the bot Secret through the
# 1P item below.
resource "freshdesk_group" "support" {
count = local.enabled ? 1 : 0
name = "FUTO Cloud"
description = "FUTO Backups support tickets, managed by yucca tf"
}
resource "onepassword_item" "group_id" {
count = local.enabled ? 1 : 0
vault = data.onepassword_vault.partition.uuid
title = "YUCCA_FRESHDESK_GROUP_ID"
category = "password"
password = tostring(freshdesk_group.support[0].id)
}
# rule_type 4 = observer (ticket updates); performer type 1 = agent. events/
# actions are raw Automations-API JSON (the provider passes them through),
# mirroring a rule read back via GET /api/v2/automations/4/rules — NB
# content_type is required, content_layout is a string, and content is a JSON
# object whose string values carry the placeholders. Scoping to yucca tickets
# happens bot-side, so the rule has no conditions.
resource "freshdesk_automation_rule" "ticket_sync" {
count = local.enabled ? 1 : 0
name = "yucca prod ticket sync"
rule_type = 4
active = true
performer = jsonencode({ type = 1 })
events = jsonencode([
{ field_name = "reply_sent" },
{ field_name = "note_type", value = "public" },
{ field_name = "status", from = "--", to = "--" },
])
actions = jsonencode([{
field_name = "trigger_webhook"
request_type = "POST"
content_type = "JSON"
content_layout = "2"
url = "https://${var.yucca_app_domain}/hooks/${random_password.webhook_path.result}"
content = { ticket_id = "{{ticket.id}}" }
custom_headers = { "x-freshdesk-secret" = random_password.webhook_secret.result }
}])
}
@@ -0,0 +1,8 @@
include "root" {
path = find_in_parent_folders("terragrunt.hcl")
}
# Freshdesk-side support wiring for the partition (docs/discord-support.md):
# webhook credentials, the agent group and the ticket-update automation rule.
# partition/region are injected by the root config (parsed from the path:
# deployment/<partition>/global/freshdesk).
@@ -0,0 +1,71 @@
variable "partition" {
description = "Partition slug, injected by terragrunt from the path (deployment/<partition>/global/freshdesk)."
type = string
}
variable "region" {
description = "Region slug (global for this partition-wide stack)."
type = string
default = null
}
variable "stack" {
description = "Stack name (freshdesk)."
type = string
default = null
}
variable "slug" {
description = "Canonical <partition>-<region> slug."
type = string
default = null
}
variable "role" {
description = "Region role (null for the global pseudo-region)."
type = string
default = null
}
variable "site_id" {
description = "Fabric site id (null for global)."
type = number
default = null
}
variable "datacenter" {
description = "Datacenter segment of the region FQDN (null for global)."
type = string
default = null
}
variable "provider_code" {
description = "Provider segment of the region FQDN (null for global)."
type = string
default = null
}
variable "domain" {
description = "Region FQDN suffix (null for global)."
type = string
default = null
}
variable "yucca_freshdesk_url" {
description = "Freshdesk base URL (manual YUCCA_FRESHDESK_URL item). Empty = the group/rule stay unmanaged."
type = string
default = ""
}
variable "yucca_freshdesk_admin_api_key" {
description = "Freshdesk API key of an ADMIN agent, used only by this stack for group/rule CRUD (manual YUCCA_FRESHDESK_ADMIN_API_KEY item; never lands in a cluster)."
type = string
sensitive = true
default = ""
}
variable "yucca_app_domain" {
description = "Public app domain the webhook rule targets. Must match APP_DOMAIN in the partition's cluster-settings.generated.yaml."
type = string
default = "backups.futo.cloud"
}
@@ -0,0 +1,19 @@
terraform {
required_version = "~> 1.11"
required_providers {
freshdesk = {
source = "registry.terraform.io/slop-place/freshdesk"
version = "~> 0.1"
}
# Webhook path + header secret generation.
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
# Mirrors the generated credentials into 1P for the talos stack to consume.
onepassword = {
source = "1Password/onepassword"
version = "~> 2.1"
}
}
}
@@ -1,46 +0,0 @@
# Freshdesk-side webhook wiring, TF-owned end to end (slop-place/freshdesk
# provider): the ticket-update automation rule and the per-env agent group.
# The rule's two secret ingredients — the capability-URL path segment and the
# x-freshdesk-secret header — are the random_password resources in
# secrets.tf, so no human ever handles the values and CI plans mask them.
# performer/events/actions are raw Automations-API JSON (the provider passes
# them through); Freshdesk 400s with field-level errors if the shape drifts.
# Scoping to discord tickets happens bot-side, so the rule has no conditions.
locals {
# yucca-manual-secrets placeholders read back literally as REPLACE_ME —
# never let that masquerade as config.
freshdesk_url = var.yucca_freshdesk_url == "REPLACE_ME" ? "" : var.yucca_freshdesk_url
freshdesk_api_key = var.yucca_freshdesk_api_key == "REPLACE_ME" ? "" : var.yucca_freshdesk_api_key
freshdesk_admin_api_key = var.yucca_freshdesk_admin_api_key == "REPLACE_ME" ? "" : var.yucca_freshdesk_admin_api_key
freshdesk_rules_enabled = local.freshdesk_url != "" && local.freshdesk_admin_api_key != ""
}
# Bot-created tickets land here (FRESHDESK_GROUP_ID in the bot Secret).
resource "freshdesk_group" "discord" {
count = local.freshdesk_rules_enabled ? 1 : 0
name = "FUTO Cloud"
description = "FUTO Backups Discord tickets, managed by yucca tf"
}
# rule_type 4 = observer (ticket updates); performer type 1 = agent.
resource "freshdesk_automation_rule" "discord_webhook" {
count = local.freshdesk_rules_enabled ? 1 : 0
name = "yucca prod discord ticket sync"
rule_type = 4
active = true
performer = jsonencode({ type = 1 })
events = jsonencode([
{ field_name = "reply_sent" },
{ field_name = "note_type", value = "public" },
{ field_name = "status", from = "--", to = "--" },
])
actions = jsonencode([{
field_name = "trigger_webhook"
request_type = "POST"
url = "https://${var.yucca_app_domain}/hooks/${random_password.yucca_freshdesk_webhook_path.result}"
content_layout = 2
content = "{\"ticket_id\": {{ticket.id}}}"
custom_headers = { "x-freshdesk-secret" = random_password.yucca_freshdesk_webhook_secret.result }
}])
}
@@ -23,10 +23,10 @@ provider "kubernetes" {
# via OP_SERVICE_ACCOUNT_TOKEN (op run). # via OP_SERVICE_ACCOUNT_TOKEN (op run).
provider "onepassword" {} provider "onepassword" {}
# Freshdesk automation rule + group (freshdesk.tf). Fallbacks keep the # TRANSITIONAL (see versions.tf): authenticates the destroys of the state-held
# provider configurable while the manual items are unfilled — never contacted, # freshdesk resources; the group/rule are now owned by the partition's
# every freshdesk resource is count-gated on the real config. # global/freshdesk stack.
provider "freshdesk" { provider "freshdesk" {
domain = coalesce(local.freshdesk_url, "https://freshdesk.invalid") domain = coalesce(local.freshdesk_url, "https://freshdesk.invalid")
api_key = coalesce(local.freshdesk_admin_api_key, "unset") api_key = coalesce(var.yucca_freshdesk_admin_api_key == "REPLACE_ME" ? "" : var.yucca_freshdesk_admin_api_key, "unset")
} }
+13 -33
View File
@@ -240,34 +240,6 @@ resource "onepassword_item" "yucca_internal_secret" {
password = random_password.yucca_internal_secret.result password = random_password.yucca_internal_secret.result
} }
# Freshdesk webhook credentials, both TF-generated and mirrored into 1P: the
# header secret rides the bot Secret; the capability-URL path segment reaches
# the HTTPRoute via cluster-secrets (below) so it never appears in git or CI.
resource "random_password" "yucca_freshdesk_webhook_secret" {
length = 48
special = false
}
resource "onepassword_item" "yucca_freshdesk_webhook_secret" {
vault = data.onepassword_vault.prod.uuid
title = "YUCCA_FRESHDESK_WEBHOOK_SECRET"
category = "password"
password = random_password.yucca_freshdesk_webhook_secret.result
}
resource "random_password" "yucca_freshdesk_webhook_path" {
length = 32
special = false
}
resource "onepassword_item" "yucca_freshdesk_webhook_path" {
vault = data.onepassword_vault.prod.uuid
title = "YUCCA_FRESHDESK_WEBHOOK_PATH"
category = "password"
password = random_password.yucca_freshdesk_webhook_path.result
}
# Substituted into the Flux tree (apps.yaml postBuild, optional Secret source): # Substituted into the Flux tree (apps.yaml postBuild, optional Secret source):
# the one config channel that bypasses the git-committed cluster-settings. # the one config channel that bypasses the git-committed cluster-settings.
@@ -277,7 +249,7 @@ resource "kubernetes_secret_v1" "cluster_secrets" {
namespace = "flux-system" namespace = "flux-system"
} }
data = { data = {
FRESHDESK_WEBHOOK_PATH = random_password.yucca_freshdesk_webhook_path.result FRESHDESK_WEBHOOK_PATH = var.yucca_freshdesk_webhook_path
} }
depends_on = [helm_release.flux_operator] depends_on = [helm_release.flux_operator]
} }
@@ -288,6 +260,13 @@ resource "kubernetes_secret_v1" "cluster_secrets" {
# empty so this Secret can land before their 1P items exist — the bot idles # empty so this Secret can land before their 1P items exist — the bot idles
# without a token, skips the archive sweep without S3 keys and leaves the # without a token, skips the archive sweep without S3 keys and leaves the
# Freshdesk sync dormant without creds. # Freshdesk sync dormant without creds.
locals {
# yucca-manual-secrets placeholders read back literally as REPLACE_ME —
# never let that masquerade as config.
freshdesk_url = var.yucca_freshdesk_url == "REPLACE_ME" ? "" : var.yucca_freshdesk_url
freshdesk_api_key = var.yucca_freshdesk_api_key == "REPLACE_ME" ? "" : var.yucca_freshdesk_api_key
}
resource "kubernetes_secret_v1" "futo_backups_bot" { resource "kubernetes_secret_v1" "futo_backups_bot" {
metadata { metadata {
name = "futo-backups-bot" name = "futo-backups-bot"
@@ -304,12 +283,13 @@ resource "kubernetes_secret_v1" "futo_backups_bot" {
DISCORD_CUSTOMER_ROLE_ID = var.yucca_discord_customer_role_id DISCORD_CUSTOMER_ROLE_ID = var.yucca_discord_customer_role_id
TRANSCRIPT_S3_ACCESS_KEY_ID = var.spice_transcripts_access_key TRANSCRIPT_S3_ACCESS_KEY_ID = var.spice_transcripts_access_key
TRANSCRIPT_S3_SECRET_ACCESS_KEY = var.spice_transcripts_secret_key TRANSCRIPT_S3_SECRET_ACCESS_KEY = var.spice_transcripts_secret_key
# REPLACE_ME-guarded locals (freshdesk.tf) — an unfilled placeholder # REPLACE_ME-guarded locals (below) — an unfilled placeholder keeps the
# keeps the sync dormant. # sync dormant. The webhook credentials and group id come from the
# partition's global/freshdesk stack via 1P.
FRESHDESK_URL = local.freshdesk_url FRESHDESK_URL = local.freshdesk_url
FRESHDESK_API_KEY = local.freshdesk_api_key FRESHDESK_API_KEY = local.freshdesk_api_key
FRESHDESK_GROUP_ID = try(tostring(freshdesk_group.discord[0].id), "") FRESHDESK_GROUP_ID = var.yucca_freshdesk_group_id
FRESHDESK_WEBHOOK_SECRET = random_password.yucca_freshdesk_webhook_secret.result FRESHDESK_WEBHOOK_SECRET = var.yucca_freshdesk_webhook_secret
} }
} }
+18 -4
View File
@@ -282,16 +282,30 @@ variable "yucca_freshdesk_api_key" {
} }
variable "yucca_freshdesk_admin_api_key" { variable "yucca_freshdesk_admin_api_key" {
description = "Freshdesk API key of an ADMIN agent, used only by the freshdesk provider to manage the automation rule and group (manual YUCCA_FRESHDESK_ADMIN_API_KEY item; never lands in the cluster). Empty = the rules are unmanaged." description = "TRANSITIONAL (see versions.tf): admin key for destroying the state-held freshdesk resources (manual YUCCA_FRESHDESK_ADMIN_API_KEY item)."
type = string type = string
sensitive = true sensitive = true
default = "" default = ""
} }
variable "yucca_app_domain" { variable "yucca_freshdesk_webhook_secret" {
description = "Public app domain the Freshdesk webhook rule targets. Must match APP_DOMAIN in the cluster-settings.generated.yaml of this cluster." description = "Webhook header secret minted by the prod/global/freshdesk stack (YUCCA_FRESHDESK_WEBHOOK_SECRET); refs stay commented in tf/.env.prod until its first apply."
type = string type = string
default = "backups.futo.cloud" sensitive = true
default = ""
}
variable "yucca_freshdesk_webhook_path" {
description = "Capability-URL path segment minted by the prod/global/freshdesk stack (YUCCA_FRESHDESK_WEBHOOK_PATH); substituted into the Flux tree via cluster-secrets."
type = string
sensitive = true
default = ""
}
variable "yucca_freshdesk_group_id" {
description = "Freshdesk agent-group id minted by the prod/global/freshdesk stack (YUCCA_FRESHDESK_GROUP_ID)."
type = string
default = ""
} }
variable "yucca_discord_guild_id" { variable "yucca_discord_guild_id" {
@@ -32,7 +32,10 @@ terraform {
source = "hashicorp/random" source = "hashicorp/random"
version = "~> 3.6" version = "~> 3.6"
} }
# Freshdesk automation rule + agent group (freshdesk.tf). # TRANSITIONAL: no freshdesk resources remain in config — the declaration
# only lets tofu destroy the state-held group/rule from the pre-
# global/freshdesk layout. Remove together with the provider block and
# yucca_freshdesk_admin_api_key after one apply.
freshdesk = { freshdesk = {
source = "registry.terraform.io/slop-place/freshdesk" source = "registry.terraform.io/slop-place/freshdesk"
version = "~> 0.1" version = "~> 0.1"
@@ -1,46 +0,0 @@
# Freshdesk-side webhook wiring, TF-owned end to end (slop-place/freshdesk
# provider): the ticket-update automation rule and the per-env agent group.
# The rule's two secret ingredients — the capability-URL path segment and the
# x-freshdesk-secret header — are the random_password resources in
# secrets.tf, so no human ever handles the values and CI plans mask them.
# performer/events/actions are raw Automations-API JSON (the provider passes
# them through); Freshdesk 400s with field-level errors if the shape drifts.
# Scoping to discord tickets happens bot-side, so the rule has no conditions.
locals {
# yucca-manual-secrets placeholders read back literally as REPLACE_ME —
# never let that masquerade as config.
freshdesk_url = var.yucca_freshdesk_url == "REPLACE_ME" ? "" : var.yucca_freshdesk_url
freshdesk_api_key = var.yucca_freshdesk_api_key == "REPLACE_ME" ? "" : var.yucca_freshdesk_api_key
freshdesk_admin_api_key = var.yucca_freshdesk_admin_api_key == "REPLACE_ME" ? "" : var.yucca_freshdesk_admin_api_key
freshdesk_rules_enabled = local.freshdesk_url != "" && local.freshdesk_admin_api_key != ""
}
# Bot-created tickets land here (FRESHDESK_GROUP_ID in the bot Secret).
resource "freshdesk_group" "discord" {
count = local.freshdesk_rules_enabled ? 1 : 0
name = "FUTO Cloud (Staging)"
description = "FUTO Backups Discord tickets from staging, managed by yucca tf"
}
# rule_type 4 = observer (ticket updates); performer type 1 = agent.
resource "freshdesk_automation_rule" "discord_webhook" {
count = local.freshdesk_rules_enabled ? 1 : 0
name = "yucca staging discord ticket sync"
rule_type = 4
active = true
performer = jsonencode({ type = 1 })
events = jsonencode([
{ field_name = "reply_sent" },
{ field_name = "note_type", value = "public" },
{ field_name = "status", from = "--", to = "--" },
])
actions = jsonencode([{
field_name = "trigger_webhook"
request_type = "POST"
url = "https://${var.yucca_app_domain}/hooks/${random_password.yucca_freshdesk_webhook_path[0].result}"
content_layout = 2
content = "{\"ticket_id\": {{ticket.id}}}"
custom_headers = { "x-freshdesk-secret" = random_password.yucca_freshdesk_webhook_secret[0].result }
}])
}
@@ -35,10 +35,10 @@ provider "kubernetes" {
# same token the rest of the stack uses); no Connect host needed. # same token the rest of the stack uses); no Connect host needed.
provider "onepassword" {} provider "onepassword" {}
# Freshdesk automation rule + group (freshdesk.tf). Fallbacks keep the # TRANSITIONAL (see versions.tf): authenticates the destroys of the state-held
# provider configurable while the manual items are unfilled — never contacted, # freshdesk resources; the group/rule are now owned by the partition's
# every freshdesk resource is count-gated on the real config. # global/freshdesk stack.
provider "freshdesk" { provider "freshdesk" {
domain = coalesce(local.freshdesk_url, "https://freshdesk.invalid") domain = coalesce(local.freshdesk_url, "https://freshdesk.invalid")
api_key = coalesce(local.freshdesk_admin_api_key, "unset") api_key = coalesce(var.yucca_freshdesk_admin_api_key == "REPLACE_ME" ? "" : var.yucca_freshdesk_admin_api_key, "unset")
} }
+13 -37
View File
@@ -237,38 +237,6 @@ resource "onepassword_item" "yucca_internal_secret" {
password = random_password.yucca_internal_secret[0].result password = random_password.yucca_internal_secret[0].result
} }
# Freshdesk webhook credentials, both TF-generated and mirrored into 1P: the
# header secret rides the bot Secret; the capability-URL path segment reaches
# the HTTPRoute via cluster-secrets (below) so it never appears in git or CI.
resource "random_password" "yucca_freshdesk_webhook_secret" {
count = local.provision_secrets ? 1 : 0
length = 48
special = false
}
resource "onepassword_item" "yucca_freshdesk_webhook_secret" {
count = local.provision_secrets ? 1 : 0
vault = data.onepassword_vault.staging[0].uuid
title = "YUCCA_FRESHDESK_WEBHOOK_SECRET"
category = "password"
password = random_password.yucca_freshdesk_webhook_secret[0].result
}
resource "random_password" "yucca_freshdesk_webhook_path" {
count = local.provision_secrets ? 1 : 0
length = 32
special = false
}
resource "onepassword_item" "yucca_freshdesk_webhook_path" {
count = local.provision_secrets ? 1 : 0
vault = data.onepassword_vault.staging[0].uuid
title = "YUCCA_FRESHDESK_WEBHOOK_PATH"
category = "password"
password = random_password.yucca_freshdesk_webhook_path[0].result
}
# Substituted into the Flux tree (apps.yaml postBuild, optional Secret source): # Substituted into the Flux tree (apps.yaml postBuild, optional Secret source):
# the one config channel that bypasses the git-committed cluster-settings. # the one config channel that bypasses the git-committed cluster-settings.
@@ -279,7 +247,7 @@ resource "kubernetes_secret_v1" "cluster_secrets" {
namespace = "flux-system" namespace = "flux-system"
} }
data = { data = {
FRESHDESK_WEBHOOK_PATH = random_password.yucca_freshdesk_webhook_path[0].result FRESHDESK_WEBHOOK_PATH = var.yucca_freshdesk_webhook_path
} }
depends_on = [helm_release.flux_operator] depends_on = [helm_release.flux_operator]
} }
@@ -289,6 +257,13 @@ resource "kubernetes_secret_v1" "cluster_secrets" {
# token and S3 keys default empty so this Secret can land before their 1P # token and S3 keys default empty so this Secret can land before their 1P
# items exist — the bot idles without a token and skips the archive sweep # items exist — the bot idles without a token and skips the archive sweep
# without S3 keys. # without S3 keys.
locals {
# yucca-manual-secrets placeholders read back literally as REPLACE_ME —
# never let that masquerade as config.
freshdesk_url = var.yucca_freshdesk_url == "REPLACE_ME" ? "" : var.yucca_freshdesk_url
freshdesk_api_key = var.yucca_freshdesk_api_key == "REPLACE_ME" ? "" : var.yucca_freshdesk_api_key
}
resource "kubernetes_secret_v1" "futo_backups_bot" { resource "kubernetes_secret_v1" "futo_backups_bot" {
count = local.provision_secrets ? 1 : 0 count = local.provision_secrets ? 1 : 0
metadata { metadata {
@@ -306,12 +281,13 @@ resource "kubernetes_secret_v1" "futo_backups_bot" {
DISCORD_CUSTOMER_ROLE_ID = var.yucca_discord_customer_role_id DISCORD_CUSTOMER_ROLE_ID = var.yucca_discord_customer_role_id
TRANSCRIPT_S3_ACCESS_KEY_ID = var.sietch_transcripts_access_key TRANSCRIPT_S3_ACCESS_KEY_ID = var.sietch_transcripts_access_key
TRANSCRIPT_S3_SECRET_ACCESS_KEY = var.sietch_transcripts_secret_key TRANSCRIPT_S3_SECRET_ACCESS_KEY = var.sietch_transcripts_secret_key
# REPLACE_ME-guarded locals (freshdesk.tf) — an unfilled placeholder # REPLACE_ME-guarded locals (below) — an unfilled placeholder keeps the
# keeps the sync dormant. # sync dormant. The webhook credentials and group id come from the
# partition's global/freshdesk stack via 1P.
FRESHDESK_URL = local.freshdesk_url FRESHDESK_URL = local.freshdesk_url
FRESHDESK_API_KEY = local.freshdesk_api_key FRESHDESK_API_KEY = local.freshdesk_api_key
FRESHDESK_GROUP_ID = try(tostring(freshdesk_group.discord[0].id), "") FRESHDESK_GROUP_ID = var.yucca_freshdesk_group_id
FRESHDESK_WEBHOOK_SECRET = random_password.yucca_freshdesk_webhook_secret[0].result FRESHDESK_WEBHOOK_SECRET = var.yucca_freshdesk_webhook_secret
# Staging shares the prod Freshdesk account; the tag keeps its tickets # Staging shares the prod Freshdesk account; the tag keeps its tickets
# filterable in the agent view. # filterable in the agent view.
FRESHDESK_TAGS = "staging" FRESHDESK_TAGS = "staging"
@@ -255,16 +255,30 @@ variable "yucca_freshdesk_api_key" {
} }
variable "yucca_freshdesk_admin_api_key" { variable "yucca_freshdesk_admin_api_key" {
description = "Freshdesk API key of an ADMIN agent, used only by the freshdesk provider to manage the automation rule and group (manual YUCCA_FRESHDESK_ADMIN_API_KEY item; never lands in the cluster). Empty = the rules are unmanaged." description = "TRANSITIONAL (see versions.tf): admin key for destroying the state-held freshdesk resources (manual YUCCA_FRESHDESK_ADMIN_API_KEY item)."
type = string type = string
sensitive = true sensitive = true
default = "" default = ""
} }
variable "yucca_app_domain" { variable "yucca_freshdesk_webhook_secret" {
description = "Public app domain the Freshdesk webhook rule targets. Must match APP_DOMAIN in the cluster-settings.generated.yaml of this cluster." description = "Webhook header secret minted by the staging/global/freshdesk stack (YUCCA_FRESHDESK_WEBHOOK_SECRET); refs stay commented in tf/.env until its first apply."
type = string type = string
default = "staging.backups.futo.cloud" sensitive = true
default = ""
}
variable "yucca_freshdesk_webhook_path" {
description = "Capability-URL path segment minted by the staging/global/freshdesk stack (YUCCA_FRESHDESK_WEBHOOK_PATH); substituted into the Flux tree via cluster-secrets."
type = string
sensitive = true
default = ""
}
variable "yucca_freshdesk_group_id" {
description = "Freshdesk agent-group id minted by the staging/global/freshdesk stack (YUCCA_FRESHDESK_GROUP_ID)."
type = string
default = ""
} }
variable "yucca_discord_guild_id" { variable "yucca_discord_guild_id" {
@@ -10,6 +10,14 @@ terraform {
source = "hashicorp/random" source = "hashicorp/random"
version = "~> 3.6" version = "~> 3.6"
} }
# TRANSITIONAL: no freshdesk resources remain in config — the declaration
# only lets tofu destroy the state-held group/rule from the pre-
# global/freshdesk layout. Remove together with the provider block and
# yucca_freshdesk_admin_api_key after one apply.
freshdesk = {
source = "registry.terraform.io/slop-place/freshdesk"
version = "~> 0.1"
}
helm = { helm = {
source = "hashicorp/helm" source = "hashicorp/helm"
version = "~> 3.1" version = "~> 3.1"
@@ -29,10 +37,5 @@ terraform {
source = "1Password/onepassword" source = "1Password/onepassword"
version = "~> 2.1" version = "~> 2.1"
} }
# Freshdesk automation rule + agent group (freshdesk.tf).
freshdesk = {
source = "registry.terraform.io/slop-place/freshdesk"
version = "~> 0.1"
}
} }
} }
@@ -0,0 +1,69 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/1password/onepassword" {
version = "2.2.1"
constraints = "~> 2.1"
hashes = [
"h1:aH5pZUimlQdiGnDLHRC49W4xnkx52wfd8XRLt69+764=",
"zh:025709a6b5f1b3685d277f2c48f7cb8b53d14b3699c1123d7e9a2135c099c533",
"zh:037fc89d150063a8aacdcab08ba26038b489fe2468d509b842d298ea59096ca6",
"zh:233777182b25faf1658e8ce171b684460983bb41cff79fb243662f3f9dc5ca6c",
"zh:2fb5ca2fc8c37b1d1c54da646ed13bf40897941fe92eece784fba496f677b533",
"zh:4b25b5ce1f694ec265e65234fc85d6bdf3810297ffeaced54ad46a1ba28142de",
"zh:5509d1e4fb7b45c63124ec66fd1f9d6757daa8bf1f7bdd724d5adb2965b61436",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:a23ba946c629ec912b2fcbf606a2eb8853626ec0e0bee749f2d39146a872c082",
"zh:a3d3024485426237d7b4a4350b12dda4d29d88f3942246a9370be35ec2a51e9e",
"zh:a6ef65544ab8fc26d468b38636407a3d2d902e35c51b648729bf97c31d1937f9",
"zh:afbe9480a0da0ad8dc514b277f1e4be36b8931f045021d05c21665ef1ac0b7c8",
"zh:b2e96e69fa9ff7e179dccdef5b785cd020eb46bb2b3d1d507d009d71be6b0c26",
"zh:ceefaede9e8a3104463523ba267e3e985b27a706f7628a9ddd37330c2ca59d4d",
"zh:ea77786bd6809ff4f8043b84a0212fec4de18b7d51bc420417ba10999ca99887",
"zh:f7d8160c3669c8ab76a2da14ea740d91a08ca23d1fb657669e52a840b2b113d9",
]
}
provider "registry.opentofu.org/hashicorp/random" {
version = "3.9.0"
constraints = "~> 3.6"
hashes = [
"h1:U8KXqGCoNI9/guYbTvzgdtVk3fRthoG0UXwm1JoEpIs=",
"zh:03f1114cc20b8913523735ab76e0f0a2b16ce13c92923a53304bf85f07fc0dbc",
"zh:105b678ee72322a3067f105d7e05e940f6143238f377f6e87ff4ec909246ac2a",
"zh:55f3bbf13ea18cbace61a706566a80f25f33fe2b1780b6f3d7b582af2a05b6d2",
"zh:63adf996db48f082f7a6351eb485e219cd88795fc71e6ec60a837263ab0d2cb1",
"zh:7e99550738a4e3cc68b8a467714b0d69371025fe95e3326d5323d026d55653e9",
"zh:8342b54af3a18a37e075eeae61be57f4de2ba71b35d95c5075d402dd2c1f289d",
"zh:83ee18e32ac9dd5fc91298554b7c4cfa4c3a1db50f4c797945637cc93c0844ae",
"zh:993ecc0adbf6bd535a59fbc9b735d8c33950e6f6eb5e621d750da9b71d65d80a",
"zh:ad722bc59d4edbf1415e827fc007c0efe6e0e9462d5568bae20b34be1058a261",
"zh:ae9448e1f87b2f9a6c5197a0e9862162ec6b137cb3a3835e11522995d8939e7c",
"zh:bc9cdd3aac784f759125c6627f6f6416e8726a1c184eb9cf3e55b9edbc94c627",
"zh:c8e35b89572ba1c40a9b20022e033a3395fb8d42e7604d50c900f193ba10382e",
"zh:e2deaa8a9975ef81d9f62baed12c41286918b0a10908e0e031f13f69a3b730a1",
"zh:ee39707557210a0ab1098aa357d2cdfe502e5a312d0dbdffb09d08facc4d3fc5",
"zh:f81afe4eb63e8aa9e0ea71be6c990f0dc69cb360e7191c0742a991f4a5081b64",
]
}
provider "registry.terraform.io/slop-place/freshdesk" {
version = "0.1.1"
constraints = "~> 0.1"
hashes = [
"h1:VyOKR4IfX4LWwW1Gv6rAa5A5x/h0IEDqKHL4hjliao8=",
"zh:19326bde07045235a0b41a1e0936a0929ee5b8a63d1856199dde34c1e2a2f641",
"zh:2221a63af314b9fae115bd455564e80105ead873856ebff4a9ae967fa91b9408",
"zh:239a2dc1433199705b587470fc86fc5d75761e2b6b2d640a112d416966676479",
"zh:4b7678d13a51cf6525ea7aec5788e2fcb96c8ef6b9f853937d224946f928ddb4",
"zh:74e33bffa31c664a3ae1cb20d098c804ff92c83fc042ec7d24b68f0ebd8f027f",
"zh:7943bf339e8f825b56c109ad92702102cdf4d07d577c48e45228e2f0d7889eaf",
"zh:7d4958ed366239294b085542859d5cc135b8041e1c5c5ab2a0b8c278c45df665",
"zh:91b8bcc4e58ba08f4e3380d07d6df32719d8464607cc1a0c56f75978cccd0cc9",
"zh:9deb669d3cd5dd598f83d51112987540c71c46f49b6d7841fd72a88fcb84f3f0",
"zh:ae3a04d72429f1b45db0cb1b1741ff007fc558c864c3b64652a48f1ab636f102",
"zh:d477d2919ff96f3d58be4df678f8eca52fb24254d53590fddca6b34d49d48215",
"zh:dd743e45b051dcff9b4f88bd99a6c5c24be9a614ee750625f900b4d0022634e8",
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
]
}
@@ -0,0 +1,31 @@
# ── Discovery contract ──────────────────────────────────────────────────────
# Single non-sensitive envelope consumed by yuctl. The credential values stay
# in 1P — only the item titles are output. See tf/README.md.
output "discovery_schema_version" {
description = "Schema version of the discovery output envelope."
value = 1
}
output "discovery" {
description = "Freshdesk payload for this partition (non-sensitive)."
value = {
schema_version = 1
partition = var.partition
region = var.region
slug = var.slug
role = var.role
stack = var.stack
stack_type = "freshdesk"
freshdesk = {
vault = "yucca_tf_${var.partition}"
group_name = "FUTO Cloud (Staging)"
rule_managed = nonsensitive(local.enabled)
item_titles = {
webhook_path = "YUCCA_FRESHDESK_WEBHOOK_PATH"
webhook_secret = "YUCCA_FRESHDESK_WEBHOOK_SECRET"
group_id = "YUCCA_FRESHDESK_GROUP_ID"
}
}
}
}
@@ -0,0 +1,99 @@
# The webhook contract, TF-owned end to end: this stack mints the capability-
# URL path segment and the x-freshdesk-secret header, mirrors them into 1P,
# and points the Freshdesk automation rule at them — no human ever handles
# the values, and the talos stack consumes them back via op:// refs (bot
# Secret + cluster-secrets). See docs/discord-support.md.
locals {
# yucca-manual-secrets placeholders read back literally as REPLACE_ME —
# never let that masquerade as config.
url = var.yucca_freshdesk_url == "REPLACE_ME" ? "" : var.yucca_freshdesk_url
admin_api_key = var.yucca_freshdesk_admin_api_key == "REPLACE_ME" ? "" : var.yucca_freshdesk_admin_api_key
enabled = local.url != "" && local.admin_api_key != ""
}
# Fallbacks keep the provider configurable while the manual items are
# unfilled — never contacted, the freshdesk resources are count-gated.
provider "freshdesk" {
domain = coalesce(local.url, "https://freshdesk.invalid")
api_key = coalesce(local.admin_api_key, "unset")
}
provider "onepassword" {}
data "onepassword_vault" "partition" {
name = "yucca_tf_${var.partition}"
}
# Generated unconditionally (no Freshdesk dependency) so the talos stack's
# op:// refs can be uncommented right after this stack's first apply.
resource "random_password" "webhook_secret" {
length = 48
special = false
}
resource "onepassword_item" "webhook_secret" {
vault = data.onepassword_vault.partition.uuid
title = "YUCCA_FRESHDESK_WEBHOOK_SECRET"
category = "password"
password = random_password.webhook_secret.result
}
resource "random_password" "webhook_path" {
length = 32
special = false
}
resource "onepassword_item" "webhook_path" {
vault = data.onepassword_vault.partition.uuid
title = "YUCCA_FRESHDESK_WEBHOOK_PATH"
category = "password"
password = random_password.webhook_path.result
}
# Bot-created tickets land here; the id reaches the bot Secret through the
# 1P item below.
resource "freshdesk_group" "support" {
count = local.enabled ? 1 : 0
name = "FUTO Cloud (Staging)"
description = "FUTO Backups support tickets from staging, managed by yucca tf"
}
resource "onepassword_item" "group_id" {
count = local.enabled ? 1 : 0
vault = data.onepassword_vault.partition.uuid
title = "YUCCA_FRESHDESK_GROUP_ID"
category = "password"
password = tostring(freshdesk_group.support[0].id)
}
# rule_type 4 = observer (ticket updates); performer type 1 = agent. events/
# actions are raw Automations-API JSON (the provider passes them through),
# mirroring a rule read back via GET /api/v2/automations/4/rules — NB
# content_type is required, content_layout is a string, and content is a JSON
# object whose string values carry the placeholders. Scoping to yucca tickets
# happens bot-side, so the rule has no conditions.
resource "freshdesk_automation_rule" "ticket_sync" {
count = local.enabled ? 1 : 0
name = "yucca staging ticket sync"
rule_type = 4
active = true
performer = jsonencode({ type = 1 })
events = jsonencode([
{ field_name = "reply_sent" },
{ field_name = "note_type", value = "public" },
{ field_name = "status", from = "--", to = "--" },
])
actions = jsonencode([{
field_name = "trigger_webhook"
request_type = "POST"
content_type = "JSON"
content_layout = "2"
url = "https://${var.yucca_app_domain}/hooks/${random_password.webhook_path.result}"
content = { ticket_id = "{{ticket.id}}" }
custom_headers = { "x-freshdesk-secret" = random_password.webhook_secret.result }
}])
}
@@ -0,0 +1,8 @@
include "root" {
path = find_in_parent_folders("terragrunt.hcl")
}
# Freshdesk-side support wiring for the partition (docs/discord-support.md):
# webhook credentials, the agent group and the ticket-update automation rule.
# partition/region are injected by the root config (parsed from the path:
# deployment/<partition>/global/freshdesk).
@@ -0,0 +1,71 @@
variable "partition" {
description = "Partition slug, injected by terragrunt from the path (deployment/<partition>/global/freshdesk)."
type = string
}
variable "region" {
description = "Region slug (global for this partition-wide stack)."
type = string
default = null
}
variable "stack" {
description = "Stack name (freshdesk)."
type = string
default = null
}
variable "slug" {
description = "Canonical <partition>-<region> slug."
type = string
default = null
}
variable "role" {
description = "Region role (null for the global pseudo-region)."
type = string
default = null
}
variable "site_id" {
description = "Fabric site id (null for global)."
type = number
default = null
}
variable "datacenter" {
description = "Datacenter segment of the region FQDN (null for global)."
type = string
default = null
}
variable "provider_code" {
description = "Provider segment of the region FQDN (null for global)."
type = string
default = null
}
variable "domain" {
description = "Region FQDN suffix (null for global)."
type = string
default = null
}
variable "yucca_freshdesk_url" {
description = "Freshdesk base URL (manual YUCCA_FRESHDESK_URL item). Empty = the group/rule stay unmanaged."
type = string
default = ""
}
variable "yucca_freshdesk_admin_api_key" {
description = "Freshdesk API key of an ADMIN agent, used only by this stack for group/rule CRUD (manual YUCCA_FRESHDESK_ADMIN_API_KEY item; never lands in a cluster)."
type = string
sensitive = true
default = ""
}
variable "yucca_app_domain" {
description = "Public app domain the webhook rule targets. Must match APP_DOMAIN in the partition's cluster-settings.generated.yaml."
type = string
default = "staging.backups.futo.cloud"
}
@@ -0,0 +1,19 @@
terraform {
required_version = "~> 1.11"
required_providers {
freshdesk = {
source = "registry.terraform.io/slop-place/freshdesk"
version = "~> 0.1"
}
# Webhook path + header secret generation.
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
# Mirrors the generated credentials into 1P for the talos stack to consume.
onepassword = {
source = "1Password/onepassword"
version = "~> 2.1"
}
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ variable "users" {
# key landing in a read-only class, as happened with nutgood/netops both at # key landing in a read-only class, as happened with nutgood/netops both at
# uid 3000). # uid 3000).
validation { validation {
condition = length(distinct([for u in var.users : u.uid if u.uid != null])) == length([for u in var.users : u.uid if u.uid != null]) condition = length(distinct([for u in var.users : u.uid if u.uid != null])) == length([for u in var.users : u.uid if u.uid != null])
error_message = "Duplicate uid across login users: Junos treats same-uid logins as one user and silently merges their classes/keys. Give every user a unique uid." error_message = "Duplicate uid across login users: Junos treats same-uid logins as one user and silently merges their classes/keys. Give every user a unique uid."
} }
} }