mirror of
https://github.com/immich-app/yucca.git
synced 2026-09-30 13:33:00 +08:00
refactor(tf): move freshdesk ownership into partition-level global/freshdesk stacks (#580)
This commit is contained in:
@@ -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_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"
|
||||
# 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"
|
||||
|
||||
@@ -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_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"
|
||||
# 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"
|
||||
|
||||
@@ -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).
|
||||
provider "onepassword" {}
|
||||
|
||||
# Freshdesk automation rule + group (freshdesk.tf). Fallbacks keep the
|
||||
# provider configurable while the manual items are unfilled — never contacted,
|
||||
# every freshdesk resource is count-gated on the real config.
|
||||
# TRANSITIONAL (see versions.tf): authenticates the destroys of the state-held
|
||||
# freshdesk resources; the group/rule are now owned by the partition's
|
||||
# global/freshdesk stack.
|
||||
provider "freshdesk" {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -240,34 +240,6 @@ resource "onepassword_item" "yucca_internal_secret" {
|
||||
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):
|
||||
# the one config channel that bypasses the git-committed cluster-settings.
|
||||
@@ -277,7 +249,7 @@ resource "kubernetes_secret_v1" "cluster_secrets" {
|
||||
namespace = "flux-system"
|
||||
}
|
||||
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]
|
||||
}
|
||||
@@ -288,6 +260,13 @@ resource "kubernetes_secret_v1" "cluster_secrets" {
|
||||
# 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
|
||||
# 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" {
|
||||
metadata {
|
||||
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
|
||||
TRANSCRIPT_S3_ACCESS_KEY_ID = var.spice_transcripts_access_key
|
||||
TRANSCRIPT_S3_SECRET_ACCESS_KEY = var.spice_transcripts_secret_key
|
||||
# REPLACE_ME-guarded locals (freshdesk.tf) — an unfilled placeholder
|
||||
# keeps the sync dormant.
|
||||
# REPLACE_ME-guarded locals (below) — an unfilled placeholder keeps the
|
||||
# sync dormant. The webhook credentials and group id come from the
|
||||
# partition's global/freshdesk stack via 1P.
|
||||
FRESHDESK_URL = local.freshdesk_url
|
||||
FRESHDESK_API_KEY = local.freshdesk_api_key
|
||||
FRESHDESK_GROUP_ID = try(tostring(freshdesk_group.discord[0].id), "")
|
||||
FRESHDESK_WEBHOOK_SECRET = random_password.yucca_freshdesk_webhook_secret.result
|
||||
FRESHDESK_GROUP_ID = var.yucca_freshdesk_group_id
|
||||
FRESHDESK_WEBHOOK_SECRET = var.yucca_freshdesk_webhook_secret
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -282,16 +282,30 @@ variable "yucca_freshdesk_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
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "yucca_app_domain" {
|
||||
description = "Public app domain the Freshdesk webhook rule targets. Must match APP_DOMAIN in the cluster-settings.generated.yaml of this cluster."
|
||||
variable "yucca_freshdesk_webhook_secret" {
|
||||
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
|
||||
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" {
|
||||
|
||||
@@ -32,7 +32,10 @@ terraform {
|
||||
source = "hashicorp/random"
|
||||
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 = {
|
||||
source = "registry.terraform.io/slop-place/freshdesk"
|
||||
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.
|
||||
provider "onepassword" {}
|
||||
|
||||
# Freshdesk automation rule + group (freshdesk.tf). Fallbacks keep the
|
||||
# provider configurable while the manual items are unfilled — never contacted,
|
||||
# every freshdesk resource is count-gated on the real config.
|
||||
# TRANSITIONAL (see versions.tf): authenticates the destroys of the state-held
|
||||
# freshdesk resources; the group/rule are now owned by the partition's
|
||||
# global/freshdesk stack.
|
||||
provider "freshdesk" {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -237,38 +237,6 @@ resource "onepassword_item" "yucca_internal_secret" {
|
||||
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):
|
||||
# the one config channel that bypasses the git-committed cluster-settings.
|
||||
@@ -279,7 +247,7 @@ resource "kubernetes_secret_v1" "cluster_secrets" {
|
||||
namespace = "flux-system"
|
||||
}
|
||||
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]
|
||||
}
|
||||
@@ -289,6 +257,13 @@ resource "kubernetes_secret_v1" "cluster_secrets" {
|
||||
# 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
|
||||
# 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" {
|
||||
count = local.provision_secrets ? 1 : 0
|
||||
metadata {
|
||||
@@ -306,12 +281,13 @@ resource "kubernetes_secret_v1" "futo_backups_bot" {
|
||||
DISCORD_CUSTOMER_ROLE_ID = var.yucca_discord_customer_role_id
|
||||
TRANSCRIPT_S3_ACCESS_KEY_ID = var.sietch_transcripts_access_key
|
||||
TRANSCRIPT_S3_SECRET_ACCESS_KEY = var.sietch_transcripts_secret_key
|
||||
# REPLACE_ME-guarded locals (freshdesk.tf) — an unfilled placeholder
|
||||
# keeps the sync dormant.
|
||||
# REPLACE_ME-guarded locals (below) — an unfilled placeholder keeps the
|
||||
# sync dormant. The webhook credentials and group id come from the
|
||||
# partition's global/freshdesk stack via 1P.
|
||||
FRESHDESK_URL = local.freshdesk_url
|
||||
FRESHDESK_API_KEY = local.freshdesk_api_key
|
||||
FRESHDESK_GROUP_ID = try(tostring(freshdesk_group.discord[0].id), "")
|
||||
FRESHDESK_WEBHOOK_SECRET = random_password.yucca_freshdesk_webhook_secret[0].result
|
||||
FRESHDESK_GROUP_ID = var.yucca_freshdesk_group_id
|
||||
FRESHDESK_WEBHOOK_SECRET = var.yucca_freshdesk_webhook_secret
|
||||
# Staging shares the prod Freshdesk account; the tag keeps its tickets
|
||||
# filterable in the agent view.
|
||||
FRESHDESK_TAGS = "staging"
|
||||
|
||||
@@ -255,16 +255,30 @@ variable "yucca_freshdesk_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
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "yucca_app_domain" {
|
||||
description = "Public app domain the Freshdesk webhook rule targets. Must match APP_DOMAIN in the cluster-settings.generated.yaml of this cluster."
|
||||
variable "yucca_freshdesk_webhook_secret" {
|
||||
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
|
||||
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" {
|
||||
|
||||
@@ -10,6 +10,14 @@ terraform {
|
||||
source = "hashicorp/random"
|
||||
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 = {
|
||||
source = "hashicorp/helm"
|
||||
version = "~> 3.1"
|
||||
@@ -29,10 +37,5 @@ terraform {
|
||||
source = "1Password/onepassword"
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ variable "users" {
|
||||
# key landing in a read-only class, as happened with nutgood/netops both at
|
||||
# uid 3000).
|
||||
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."
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user