feat(iac): scaffolding for Archera integration perms across AWS/Azure/GCP (closes #305) by cristim · Pull Request #310 · LeanerCloud/cloud-commitments-cli · GitHub
Skip to content

feat(iac): scaffolding for Archera integration perms across AWS/Azure/GCP (closes #305) - #310

Closed
cristim wants to merge 3 commits into
feat/multicloud-web-frontendfrom
feat/issue-305-archera-iac-perms
Closed

cristim wants to merge 3 commits into
feat/multicloud-web-frontendfrom
feat/issue-305-archera-iac-perms

Conversation

@cristim

@cristim cristim commented May 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds enable_archera = false scaffolding to all three cloud environments (AWS, Azure, GCP) so non-Archera customers see no drift. When a customer sets enable_archera = true, the IaC creates the minimal identity grants needed by Archera's integration.

SCOPE LIST IS PROVISIONAL — the exact IAM permissions/RBAC actions required by Archera have not yet been confirmed against Archera's integration docs. The lists in each archera.tf are best-effort read-only cost data + commitment purchase scopes. Do NOT merge with enable_archera = true in any environment until @cristim has confirmed the scope list against Archera's actual onboarding guide.

What's in each cloud

AWS (terraform/environments/aws/archera.tf)

  • New variables: enable_archera (bool, default false), archera_aws_account_id (string, default "")
  • When enabled: cross-account aws_iam_role (cudly-archera-integration) trusted by arn:aws:iam::<archera_aws_account_id>:root, with an aws_iam_policy covering:
    • Read-only: Cost Explorer (ce:Get*, ce:List*), CUR (cur:DescribeReportDefinitions), Reserved Instances (ec2:Describe*), Savings Plans (savingsplans:Describe*)
    • Purchase-execution (provisional): ec2:PurchaseReservedInstancesOffering, ec2:ModifyReservedInstances, savingsplans:CreateSavingsPlan
  • ExternalId condition stub is commented in for easy activation once Archera provides it

Azure (terraform/environments/azure/archera.tf)

  • New variables: enable_archera, archera_azure_sp_object_id
  • When enabled: azurerm_role_definition (custom, subscription-scoped) with Microsoft.CostManagement/*/read, Microsoft.Consumption/*/read, Microsoft.Billing/*/read, Microsoft.Capacity/reservations/read, Microsoft.Capacity/reservationOrders/read|write
  • azurerm_role_assignment to Archera's service principal (principal_type = "ServicePrincipal")

GCP (terraform/environments/gcp/archera.tf)

  • New variables: enable_archera, archera_gcp_service_account
  • When enabled: google_project_iam_custom_role (stage=BETA, project-scoped) with billing read, recommender CUD read + create
  • google_project_iam_member binding to Archera's service account

IAM split placement

Per the repo's bootstrap-vs-runtime split (CLAUDE.md §"CI/CD IAM"): Archera is a runtime integration — it reads cost telemetry and executes purchases during normal operation, not during Terraform deploys. All three archera.tf files therefore live in the main environment (alongside compute.tf, database.tf) rather than in ci-cd-permissions/.

Verification

  • terraform fmt -check clean on all 3 environments
  • terraform validate passes on all 3 environments with enable_archera = false (default, count=0)
  • No existing .tfvars files modified — only .example files updated

Out of scope (follow-up PRs)

Per the issue body's suggestion of three per-cloud sub-PRs: this single scaffolding PR lands the structural gating and provisional lists for all three clouds at once (lower review risk since nothing is enabled by default). Separate follow-up PRs per cloud can narrow the scope list once Archera's docs are consulted.

Test plan

  • @cristim confirms the scope list against Archera's integration docs (or narrows it)
  • Once confirmed, a follow-up PR flips enable_archera = true in the target environment's tfvars and runs terraform plan — no new resources should appear for other environments
  • AWS: plan with enable_archera = true creates 3 resources: role, policy, attachment
  • Azure: plan creates 2 resources: role definition, role assignment
  • GCP: plan creates 2 resources: custom role, IAM member

Summary by CodeRabbit

  • New Features

    • Added optional Archera integration for AWS, Azure, and GCP environments (disabled by default). Can be enabled via environment variables and includes an optional "purchase actions" flag to permit buy-side operations.
  • Documentation

    • Added provider-specific README guidance covering enablement steps, required identifiers, and a summary of resources that will be created when the integration is enabled.

…/GCP (closes #305)

Adds enable_archera = false tfvars flag to each cloud environment. When
flipped to true, each cloud conditionally creates:

- AWS: cross-account IAM role (cudly-archera-integration) with least-
  privilege Cost Explorer read + RI/SP purchase policy
- Azure: custom RBAC role (CUDly Archera Integration) with cost read +
  reservation purchase actions, assigned to Archera's service principal
- GCP: custom project IAM role (cudlyArcheraIntegration) with billing
  read + CUD purchase permissions, granted to Archera's service account

All permission lists are clearly marked PROVISIONAL — they must be
confirmed against Archera's integration docs before any environment sets
enable_archera = true. TODO(@cristim) comments in archera.tf per cloud
call this out explicitly.

Per the bootstrap-vs-runtime IAM split: Archera is a runtime integration
(cost telemetry reads + commitment purchases during normal operation), so
archera.tf lives in the main environment alongside compute.tf, NOT in
ci-cd-permissions/. README addendums added to all three cloud envs.

terraform fmt and terraform validate pass for all three environments with
the flag both on and off.
@coderabbitai

coderabbitai Bot commented May 5, 2026 •

Copy link
Copy Markdown
Contributor

@cristim cristim added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-quarter Within the quarter impact/many Affects most users effort/m Days type/feat New capability triaged Item has been triaged labels May 5, 2026
@cristim

cristim commented May 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
terraform/environments/azure/archera.tf (1)

55-59: ⚡ Quick win

Separate purchase capability behind a second explicit flag.

enable_archera currently enables both telemetry read and purchase write. A dedicated enable_archera_purchase_actions (default false) reduces accidental financial writes during initial rollout.

💡 Proposed refactor
 resource "azurerm_role_definition" "archera_integration" {
@@
   permissions {
-    actions = [
+    actions = concat([
       # ── Read-only: Cost Management ──────────────────────────────────────
@@
       # ── Read-only: Reserved Instances ─────────────────────────────────
@@
-      # ── Purchase-execution: Reserved Instances ────────────────────────
-      # TODO(`@cristim`): enable only after confirming approval workflow with
-      # Archera (i.e. Archera requires customer approval before purchases).
-      "Microsoft.Capacity/reservationOrders/write",
-    ]
+    ], var.enable_archera_purchase_actions ? [
+      "Microsoft.Capacity/reservationOrders/write",
+    ] : [])
     not_actions = []
   }
# variables.tf (add)
variable "enable_archera_purchase_actions" {
  description = "Allow Archera purchase actions (RI/SP). Keep false until approval workflow is confirmed."
  type        = bool
  default     = false
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@terraform/environments/azure/archera.tf` around lines 55 - 59, Current setup
ties telemetry and purchase permissions to a single flag (enable_archera); add a
new variable enable_archera_purchase_actions (bool, default false) and change
the permission assembly where the write permission
"Microsoft.Capacity/reservationOrders/write" is added so that it is included
only when both enable_archera and enable_archera_purchase_actions are true;
update any places referencing enable_archera to leave read/telemetry behavior
unchanged and gate purchase-related actions/role bindings on
enable_archera_purchase_actions in addition to enable_archera.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@terraform/environments/aws/archera.tf`:
- Around line 133-155: Split the high-risk purchase permissions (the statements
with Sid "ReservedInstancesPurchase" and "SavingsPlansPurchase") out of the
existing Archera IAM policy and move them into a new, separate IAM policy
resource; add a new boolean input variable enable_archera_purchase_actions and
conditionally create/attach that new purchase policy only when
enable_archera_purchase_actions is true, leaving the original enable_archera =
true behavior to continue granting only read/telemetry permissions; update any
IAM attachment/module invocation that currently uses the single policy to
reference both policies conditionally and preserve the TODO comments about
requiring Archera approval.
- Around line 32-47: Update the Archera role trust policy to require
sts:ExternalId by un-commenting and enforcing the Condition block (add the
StringEquals = { "sts:ExternalId" = var.archera_external_id } clause in the
assume_role_policy Statement for "ArcheraAssumeRole") so the trust policy always
validates ExternalId when Archera assumes the role; additionally add a new
Terraform variable archera_external_id (string) and in the variables.tf add
validation that it is non-empty and required whenever enable_archera is true
(use a validation rule or conditional check to fail plan/apply if enable_archera
&& var.archera_external_id == ""), referencing the existing enable_archera flag
and the archera_external_id variable in the module.

In `@terraform/environments/azure/variables.tf`:
- Around line 545-556: Add a Terraform variable validation for
archera_azure_sp_object_id that enforces a non-empty, well-formed value when
enable_archera is true: inside the variable "archera_azure_sp_object_id"
declaration add a validation block that checks (var.enable_archera == false) OR
(the value is not empty and matches a GUID/UUID pattern) and return a clear
error message if the check fails; reference var.enable_archera and
var.archera_azure_sp_object_id in the validation to ensure early, descriptive
failure instead of deferring to apply-time role assignment.

In `@terraform/environments/gcp/archera.tf`:
- Around line 55-60: The listed permission
recommender.commitmentUtilizationInsights.update grants write access and should
be removed from the read-only block; edit the array where
"recommender.commitmentUtilizationInsights.update" appears (near the read-only
Recommender / Committed Use Discounts section) and delete that entry so only
"recommender.commitmentUtilizationInsights.get" and
"recommender.commitmentUtilizationInsights.list" remain, preserving formatting
and comments around the block.

In `@terraform/environments/gcp/variables.tf`:
- Around line 493-506: Add a validation block to the variable
"archera_gcp_service_account" so Terraform will fail fast when enable_archera is
true and the service account is empty or malformed; specifically, add a
conditional check that if var.enable_archera == true then
var.archera_gcp_service_account must be non-empty and match an
email/service-account pattern (e.g., user@project.iam.gserviceaccount.com) to
prevent creating a member value of "serviceAccount:". Update the variable
"archera_gcp_service_account" definition to include the validation rule and an
appropriate error_message referencing the variable and the enable_archera flag
so callers know to provide a valid service account email before applying.

---

Nitpick comments:
In `@terraform/environments/azure/archera.tf`:
- Around line 55-59: Current setup ties telemetry and purchase permissions to a
single flag (enable_archera); add a new variable enable_archera_purchase_actions
(bool, default false) and change the permission assembly where the write
permission "Microsoft.Capacity/reservationOrders/write" is added so that it is
included only when both enable_archera and enable_archera_purchase_actions are
true; update any places referencing enable_archera to leave read/telemetry
behavior unchanged and gate purchase-related actions/role bindings on
enable_archera_purchase_actions in addition to enable_archera.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eccb27a1-a829-47ef-8961-73ecc27f9382

📥 Commits

Reviewing files that changed from the base of the PR and between f83c914 and e869eba.

📒 Files selected for processing (12)
  • terraform/environments/aws/README.md
  • terraform/environments/aws/archera.tf
  • terraform/environments/aws/dev.tfvars.example
  • terraform/environments/aws/variables.tf
  • terraform/environments/azure/README.md
  • terraform/environments/azure/archera.tf
  • terraform/environments/azure/dev.tfvars.example
  • terraform/environments/azure/variables.tf
  • terraform/environments/gcp/README.md
  • terraform/environments/gcp/archera.tf
  • terraform/environments/gcp/dev.tfvars.example
  • terraform/environments/gcp/variables.tf

Comment thread terraform/environments/aws/archera.tf
Comment thread terraform/environments/aws/archera.tf
Comment thread terraform/environments/azure/variables.tf
Comment thread terraform/environments/gcp/archera.tf Outdated
Comment thread terraform/environments/gcp/variables.tf
…lit, validation

AWS:
- enforce ExternalId in assume-role trust policy (confused-deputy protection)
- split archera_purchase into a separate IAM policy gated by
  enable_archera_purchase_actions (default false) so read-only rollouts
  never accidentally include financial writes
- add archera_external_id variable (sensitive, required when enable_archera=true)
- add enable_archera_purchase_actions variable (bool, default false)

Azure:
- add UUID validation on archera_azure_sp_object_id when enable_archera=true
- add enable_archera_purchase_actions variable; gate reservationOrders/write
  behind it via concat() so the initial rollout is read-only

GCP:
- remove recommender.commitmentUtilizationInsights.update (write action
  incorrectly placed in read-only block)
- add email format validation on archera_gcp_service_account when
  enable_archera=true
- add enable_archera_purchase_actions variable; gate recommender.commitments.create
  behind it via concat()

All environments: dev.tfvars.example updated to include
enable_archera_purchase_actions = false

terraform fmt + validate pass for all three clouds.
@cristim

cristim commented May 5, 2026

Copy link
Copy Markdown
Member Author

CodeRabbit review — round 1 fixes (commit f3ba36a)

All 5 Actionable items addressed, plus the Nitpick:

Addressed

AWS

  • ✅ ExternalId enforcement: added archera_external_id variable (sensitive = true, validation requires non-empty when enable_archera = true); trust policy now unconditionally requires StringEquals { sts:ExternalId } (confused-deputy protection)
  • ✅ Purchase/read split: aws_iam_policy.archera_read (attached when enable_archera = true) and aws_iam_policy.archera_purchase (attached only when both enable_archera and enable_archera_purchase_actions are true) — financial writes are never included at read-only rollout
  • ✅ Added enable_archera_purchase_actions variable (bool, default false)

Azure

  • ✅ UUID validation on archera_azure_sp_object_id when enable_archera = true (regex ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-...{12}$)
  • ✅ Added enable_archera_purchase_actions variable; Microsoft.Capacity/reservationOrders/write gated via concat() behind the flag

GCP

  • ✅ Removed recommender.commitmentUtilizationInsights.update (write action incorrectly placed in the read-only block)
  • ✅ Email format validation on archera_gcp_service_account when enable_archera = true (regex ^[^@]+@[^.]+\.iam\.gserviceaccount\.com$)
  • ✅ Added enable_archera_purchase_actions variable; recommender.commitments.create gated via concat() behind the flag

All clouds: dev.tfvars.example updated with enable_archera_purchase_actions = false

terraform fmt -check + terraform validate pass for all three clouds.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@cristim: Excellent work on the round 1 fixes! The changes look well-structured:

  • AWS: The ExternalId enforcement is a critical security improvement for confused-deputy protection, and the read/purchase policy split is a great least-privilege approach.
  • Azure: UUID validation and the concat()-gated write permission are solid guards.
  • GCP: Removing the misplaced write action and adding email format validation clean things up nicely.

Triggering a fresh review of commit f3ba36a now.

[review]

@cristim

cristim commented May 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

The 10 min since the previous re-review trigger have passed without a posted review. Please review commit f3ba36a which addresses the 5 actionable items + 1 nitpick from your round-1 review (ExternalId enforcement, AWS read/purchase policy split, Azure UUID validation, GCP removed update perm, GCP email validation, all clouds gained enable_archera_purchase_actions flag).

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@cristim: Understood! Triggering a full review of PR #310 now, covering commit f3ba36a and all the changes addressed in round 1:

  • AWS: ExternalId enforcement in the trust policy (confused-deputy protection), read/purchase IAM policy split (archera_read vs archera_purchase), and new enable_archera_purchase_actions variable.
  • Azure: UUID validation on archera_azure_sp_object_id, Microsoft.Capacity/reservationOrders/write gated behind enable_archera_purchase_actions.
  • GCP: Removed recommender.commitmentUtilizationInsights.update from the read block, email format validation on archera_gcp_service_account, recommender.commitments.create gated behind enable_archera_purchase_actions.
  • All clouds: dev.tfvars.example updated with enable_archera_purchase_actions = false.

[full_review]

@cristim

cristim commented May 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai resolve

The previous [full_review] acknowledgment 25 min ago has not produced a posted review. Please re-attempt the full review now of commit f3ba36a. If you've already verified all round-1 actionables are addressed and have no new findings, please acknowledge with an explicit comment so this PR can be marked review-complete.

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@terraform/environments/aws/README.md`:
- Around line 48-76: Update the README enablement example and resource table to
match the current Terraform split: add the required archera_external_id variable
and mention the optional enable_archera_purchase_actions flag; replace
references to the removed aws_iam_policy.archera_integration and
aws_iam_role_policy_attachment.archera_integration with
aws_iam_policy.archera_read and aws_iam_role_policy_attachment.archera_read, and
add entries for aws_iam_policy.archera_purchase and
aws_iam_role_policy_attachment.archera_purchase (note purchase policy is
conditional on enable_archera_purchase_actions), and ensure the example shows
enabling enable_archera = true alongside archera_aws_account_id and
archera_external_id.

In `@terraform/environments/aws/variables.tf`:
- Around line 503-515: Add a validation block to the variable
"archera_aws_account_id" so that when set it must be a 12-digit AWS account ID;
specifically ensure the value is either empty or matches the regex for twelve
digits (e.g., ^\d{12}$) and provide a clear message referencing enable_archera;
also update aws/archera.tf to guard generation of the trust principal ARN with
var.enable_archera to avoid building an ARN from an empty/malformed ID.

In `@terraform/environments/azure/README.md`:
- Around line 43-70: Update the README's "Enabling Archera" section to
accurately document the purchase-action flag: clarify that reservation purchase
write permission ("Microsoft.Capacity/reservationOrders/write") is only added
when enable_archera_purchase_actions = true, show the optional tfvars entry for
enable_archera_purchase_actions, and adjust the table entry for
azurerm_role_definition.archera_integration[0] to state it provides read-only
cost access by default and only adds RI purchase writes when
enable_archera_purchase_actions is enabled; reference the enable_archera and
archera_azure_sp_object_id example and the
azurerm_role_definition.archera_integration[0] and
azurerm_role_assignment.archera_integration[0] resources so operators review the
correct permission set before flipping flags.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 13a8b046-446b-4011-a64a-06ddc17c2758

📥 Commits

Reviewing files that changed from the base of the PR and between f83c914 and f3ba36a.

📒 Files selected for processing (12)
  • terraform/environments/aws/README.md
  • terraform/environments/aws/archera.tf
  • terraform/environments/aws/dev.tfvars.example
  • terraform/environments/aws/variables.tf
  • terraform/environments/azure/README.md
  • terraform/environments/azure/archera.tf
  • terraform/environments/azure/dev.tfvars.example
  • terraform/environments/azure/variables.tf
  • terraform/environments/gcp/README.md
  • terraform/environments/gcp/archera.tf
  • terraform/environments/gcp/dev.tfvars.example
  • terraform/environments/gcp/variables.tf

Comment thread terraform/environments/aws/README.md Outdated
Comment thread terraform/environments/aws/variables.tf
Comment thread terraform/environments/azure/README.md
…review

Address 3 actionable findings from CodeRabbit's review of f3ba36a:

1. AWS README — sync with read/purchase split: add `archera_external_id`
   to enablement example, mention optional `enable_archera_purchase_actions`
   flag, replace stale `archera_integration` policy/attachment table entries
   with `archera_read` + `archera_purchase`.
2. AWS variables.tf — add validation block to `archera_aws_account_id`
   that fails fast if the value isn't a 12-digit account ID when
   `enable_archera = true` (avoids building an invalid trust principal ARN
   at apply time).
3. Azure README — clarify that reservation purchase write permission is
   gated on `enable_archera_purchase_actions = true`, not `enable_archera`
   alone; adjust the role-definition row to match.

`terraform validate` passes for AWS and Azure with the new validation block.
@cristim

cristim commented May 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

CR-pass-2 fixes pushed in f8daec6:

  • AWS README — sync with read/purchase split (added archera_external_id + noted optional enable_archera_purchase_actions; replaced stale archera_integration table entries with archera_read + archera_purchase)
  • AWS variables.tf — added 12-digit archera_aws_account_id validation gated on enable_archera = true
  • Azure README — clarified purchase-write permission is conditional on enable_archera_purchase_actions

terraform validate clean for AWS and Azure.

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@cristim: Thanks for the CR-pass-2 summary! Triggering a review of commit f8daec6 now, covering:

  • AWS README: Synced with the read/purchase policy split, archera_external_id added, optional enable_archera_purchase_actions noted, and stale archera_integration table entries replaced with archera_read + archera_purchase.
  • AWS variables.tf: 12-digit numeric validation for archera_aws_account_id gated on enable_archera = true.
  • Azure README: Clarified that the reservationOrders/write permission is conditional on enable_archera_purchase_actions.

[review]

@cristim

cristim commented May 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

The previous @coderabbitai review ack at 21:34 has not produced a posted review in 30+ minutes. Please run a full review on the latest commit (f8daec6) — the changes are docs (AWS README + Azure README) plus a 12-digit validation block on archera_aws_account_id, addressing the 3 actionable items from your 21:22 review.

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@cristim: Apologies for the delays — triggering a full review of commit f8daec6 now. The scope I'll cover:

  • AWS README: Sync with read/purchase policy split — archera_external_id documented, optional enable_archera_purchase_actions noted, stale archera_integration table entries replaced with archera_read + archera_purchase.
  • AWS variables.tf: 12-digit numeric validation on archera_aws_account_id gated on enable_archera = true.
  • Azure README: Clarification that reservationOrders/write is conditional on enable_archera_purchase_actions.

[full_review]

@cristim

cristim commented May 5, 2026

Copy link
Copy Markdown
Member Author

CR loop complete — all 3 actionable items addressed

# File CR finding Fix in f8daec6
1 terraform/environments/aws/README.md docs stale post read/purchase split Added archera_external_id + optional enable_archera_purchase_actions to the enablement example; replaced archera_integration table entries with archera_read + archera_purchase (purchase conditional on the flag)
2 terraform/environments/aws/variables.tf missing 12-digit validation on archera_aws_account_id Added validation block: ^\d{12}$ regex with trimspace, gated on !var.enable_archera || .... terraform validate clean.
3 terraform/environments/azure/README.md docs imply purchase writes always granted Clarified that Microsoft.Capacity/reservationOrders/write is conditional on enable_archera_purchase_actions = true; added the optional flag to the tfvars snippet; updated the azurerm_role_definition row

CI: all 4 checks green (sanity AWS, sanity Azure, frontend build, pre-commit).
Mergeable: CLEAN.

Per-thread fix confirmations posted above (replies to each CR comment with the specific resolution).

@cristim

cristim commented May 6, 2026

Copy link
Copy Markdown
Member Author

Architectural pivot — shared module + thin caller

After discussion with @cristim and a second agent, the consensus is to NOT keep this PR's Archera implementation as inline terraform/environments/<cloud>/archera.tf files. Both the CUDly self-deploy (this PR) AND the customer-facing federation bundles (#314) need Archera permissions, so we want a single source of truth rather than two parallel implementations + a parity-script.

New shape

iac/modules/archera/
├── aws/
│   ├── main.tf          # role + read policy + optional purchase policy
│   ├── variables.tf     # enable_archera, archera_aws_account_id,
│   │                    # archera_external_id, enable_archera_purchase_actions
│   └── outputs.tf
├── azure/                # custom RBAC role + assignment
├── gcp/                  # service account + IAM bindings
└── scope.{aws,azure,gcp}.yaml   # canonical permission list (the source of truth)

Both Terraform call sites become thin module-wrapper files:

# terraform/environments/aws/archera.tf  (this PR — was inline)
module "archera" {
  source                          = "../../iac/modules/archera/aws"
  enable_archera                  = var.enable_archera
  archera_aws_account_id          = var.archera_aws_account_id
  archera_external_id             = var.archera_external_id
  enable_archera_purchase_actions = var.enable_archera_purchase_actions
}

# iac/federation/aws-target/terraform/archera.tf  (#314 — same module, same shape)
module "archera" { source = "../../../modules/archera/aws"; ... }

Non-Terraform formats (CFN/Bicep/ARM, used by federation bundles) load the action list from scope.<cloud>.yaml at bundle-render time on our side (deploy-time Fn::Transform / Bicep loadJsonContent isn't viable for customer-distributed bundles — must be pre-baked).

What this means for this PR

This PR will be reshaped to land:

  • New iac/modules/archera/{aws,azure,gcp}/ module trees
  • New iac/modules/archera/scope.{aws,azure,gcp}.yaml source-of-truth files
  • terraform/environments/<cloud>/archera.tf rewritten as thin module callers (replaces the current inline implementation)

CFN/Bicep/ARM rendering, federation bundle plumbing, frontend checkbox, and the parity check are scoped to #314 — not this PR.

CI gate

scripts/check-archera-scope.sh (lands with #314) diffs the rendered CFN/Bicep/ARM action lists against scope.<cloud>.yaml. Terraform parity is automatic — both call sites consume the same module — so no Terraform-vs-Terraform diff is needed. This replaces the earlier check-archera-parity.sh proposal in #314's body.

Coordination

A consensus sync has been posted to the multi-agent bus at ~/.claude/agent-comms/messages/sync-2026-05-06T010500Z-archera-architecture-agreed.md so any agent picking up follow-up Archera work has the same context.

@cristim

cristim commented May 6, 2026

Copy link
Copy Markdown
Member Author

Correction to my earlier comment

Re-verified the bundle-flag worktree state with a parallel agent (pr-314-resume):

Coordination record: see ~/.claude/agent-comms/messages/sync-2026-05-06T232500Z-archera-agreement-confirmed.md.

@cristim cristim closed this May 6, 2026
@cristim
cristim deleted the feat/issue-305-archera-iac-perms branch May 6, 2026 12:14
@cristim

cristim commented May 6, 2026

Copy link
Copy Markdown
Member Author

cristim added a commit that referenced this pull request May 11, 2026
…cation pages (closes #314)

Introduces the Archera Insurance CTA at the two high-intent moments
where a user is about to commit money, plus two education pages that
explain what Archera Insurance is and how the integration works.

**New module — frontend/src/archera.ts**
- `renderArcheraCTA()`: returns a small, muted <p> with an inline
  <button> that reads "Worried about committing? Archera Insurance
  underwrites commitment-overuse — learn how it works →". Placed
  below the approval note in the Purchase modal, and before the
  Save/Cancel row in Plan-creation modals.
- `openArcheraPage('what-is-archera' | 'how-it-works')`: renders a
  full-viewport overlay panel using DOM methods only (no innerHTML).
  Doesn't touch SPA routing state or the URL.
- `closeArcheraPage()`: hides + clears the overlay.
- `ARCHERA_SIGNUP_URL = 'https://archera.ai/signup?mode=cudly'` for
  attribution-tagged referral links.

**Page A — "What is Archera Insurance?"**
Plain-language explainer: commitment-overuse insurance, how Archera
earns (sharing-of-savings), when it makes sense, what CUDly's role is.
Cross-links to Page B. Signup link at bottom (target=_blank,
rel="noopener noreferrer").

**Page B — "How the CUDly ↔ Archera integration works"**
Step-by-step: sign up → Archera generates pre-filled IaC for AWS/GCP
(or OAuth consent for Azure) → Archera ingests cost data → purchase
commitments normally through CUDly. Disclaimers: Archera is
third-party, no credentials stored in CUDly. Cross-links to Page A.
Signup link at bottom.

TODO(@cristim) markers flag all copy that needs final review.

**Modal integration**
- `recommendations.ts / openPurchaseModal`: appends `renderArcheraCTA()`
  inside `#purchase-details`, below the approval note.
- `plans.ts / openCreatePlanModal + openNewPlanModal`: calls
  `injectPlanModalCTA()` (guarded against duplication with an id
  check) to insert the CTA before `.modal-buttons` in `#plan-form`.

**CSS (styles/modals.css)**: `.archera-cta`, `.archera-cta-link`,
`#archera-page-container`, `.archera-page-inner`, `.archera-page-back`,
`.archera-signup-block`, `.archera-signup-btn`, `.archera-signup-note`.

**HTML (index.html)**: adds `<div id="archera-page-container"
role="region" aria-label="Archera Insurance information">` (hidden
by default) before the modal stack.

**Tests (archera.test.ts)**: 26 assertions covering renderArcheraCTA
structure, page A/B content + crosslinks + signup link attributes,
closeArcheraPage, and CTA injection in both Purchase and Plan modals.

Nothing in backend/, cmd/, internal/, iac/, terraform/, or scripts/ is
touched. This is a frontend-only change. Supersedes PR #310 and PR #315
(closed without merging; earlier iterations bundled IAM/IaC which was
rejected by Archera-side review — see issue #314 for history).
cristim added a commit that referenced this pull request May 13, 2026
…es + email mentions (closes #314) (#336)

* feat(frontend): Archera Insurance CTA in purchase + plan modals + education pages (closes #314)

Introduces the Archera Insurance CTA at the two high-intent moments
where a user is about to commit money, plus two education pages that
explain what Archera Insurance is and how the integration works.

**New module — frontend/src/archera.ts**
- `renderArcheraCTA()`: returns a small, muted <p> with an inline
  <button> that reads "Worried about committing? Archera Insurance
  underwrites commitment-overuse — learn how it works →". Placed
  below the approval note in the Purchase modal, and before the
  Save/Cancel row in Plan-creation modals.
- `openArcheraPage('what-is-archera' | 'how-it-works')`: renders a
  full-viewport overlay panel using DOM methods only (no innerHTML).
  Doesn't touch SPA routing state or the URL.
- `closeArcheraPage()`: hides + clears the overlay.
- `ARCHERA_SIGNUP_URL = 'https://archera.ai/signup?mode=cudly'` for
  attribution-tagged referral links.

**Page A — "What is Archera Insurance?"**
Plain-language explainer: commitment-overuse insurance, how Archera
earns (sharing-of-savings), when it makes sense, what CUDly's role is.
Cross-links to Page B. Signup link at bottom (target=_blank,
rel="noopener noreferrer").

**Page B — "How the CUDly ↔ Archera integration works"**
Step-by-step: sign up → Archera generates pre-filled IaC for AWS/GCP
(or OAuth consent for Azure) → Archera ingests cost data → purchase
commitments normally through CUDly. Disclaimers: Archera is
third-party, no credentials stored in CUDly. Cross-links to Page A.
Signup link at bottom.

TODO(@cristim) markers flag all copy that needs final review.

**Modal integration**
- `recommendations.ts / openPurchaseModal`: appends `renderArcheraCTA()`
  inside `#purchase-details`, below the approval note.
- `plans.ts / openCreatePlanModal + openNewPlanModal`: calls
  `injectPlanModalCTA()` (guarded against duplication with an id
  check) to insert the CTA before `.modal-buttons` in `#plan-form`.

**CSS (styles/modals.css)**: `.archera-cta`, `.archera-cta-link`,
`#archera-page-container`, `.archera-page-inner`, `.archera-page-back`,
`.archera-signup-block`, `.archera-signup-btn`, `.archera-signup-note`.

**HTML (index.html)**: adds `<div id="archera-page-container"
role="region" aria-label="Archera Insurance information">` (hidden
by default) before the modal stack.

**Tests (archera.test.ts)**: 26 assertions covering renderArcheraCTA
structure, page A/B content + crosslinks + signup link attributes,
closeArcheraPage, and CTA injection in both Purchase and Plan modals.

Nothing in backend/, cmd/, internal/, iac/, terraform/, or scripts/ is
touched. This is a frontend-only change. Supersedes PR #310 and PR #315
(closed without merging; earlier iterations bundled IAM/IaC which was
rejected by Archera-side review — see issue #314 for history).

* feat(frontend,email): address CR findings + scope additions (issue #314)

Frontend (CR fixes):
- archera.ts: save/restore focus around overlay open/close (CR: focus
  restoration on closeArcheraPage)
- modals.css: raise #archera-page-container z-index from 900 to 1100
  so overlay sits above .modal layers (CR: z-index below modals)
- archera.test.ts: add global afterEach DOM teardown to prevent stale-
  node collisions between test suites (CR: missing teardown)

Frontend (scope additions from coordinator):
- archera.ts: update renderArcheraCTA copy to "Insure this commitment/
  plan with Archera →"; add transparency disclosure section (Page A:
  "Why is CUDly telling me about this?", Page B: "Disclosure") carrying
  two non-negotiable facts: CUDly fully functional without Archera +
  Archera sponsors CUDly; add handleArcheraDeeplink() for deep-link
  routing from email; export ARCHERA_PAGE_A/B_PATH constants
- app.ts: wire handleArcheraDeeplink() into init() after
  handlePurchaseDeeplink()
- plans.ts: pass 'plan' context to renderArcheraCTA in injectPlanModalCTA
- archera.test.ts: update CTA copy assertions; add disclosure tests (6);
  add handleArcheraDeeplink tests (3); total: 41 tests, all passing

Backend (email templates — scope addition):
- sender.go: add ArcheraEducationURL field to NotificationData; empty
  value silently omits the Archera block for backward compat
- templates.go: add conditional Archera 7-day enrollment block to
  purchaseApprovalRequestTemplate (plain-text), purchaseApprovalRequest-
  HTMLTemplate (HTML), and purchaseConfirmationTemplate
- handler_purchases.go, execution.go: populate ArcheraEducationURL as
  DashboardURL + "/archera-insurance" matching ARCHERA_PAGE_A_PATH
- template_renderers_test.go: add 4 Go tests asserting Archera content
  in all three modified templates + absence test when URL is empty

* refactor(api): extract archeraEducationURL helper to reduce sendPurchaseApprovalEmail cyclomatic complexity

The if dashboardBase != "" guard added in the previous commit pushed
sendPurchaseApprovalEmail from complexity 10 to 11, exceeding the
gocyclo pre-commit threshold. Extracted the guard into a package-private
archeraEducationURL helper; sendPurchaseApprovalEmail calls it inline
without a branch, restoring complexity to 10.

* refactor(archera): post-action offer modal + single education page

Restructures the Archera Insurance flow into a post-action offer modal,
collapses the previously split education pages into one, and tightens
the copy.

Frontend changes:
- Replace the inline "Insure this {commitment,plan} with Archera →" CTA
  inside the Purchase and Plan-creation modals with a new post-action
  offer modal (openArcheraOfferModal). The modal pops after a successful
  Send-for-Approval submission (app.ts handleExecutePurchase +
  handleFanOutExecute) or plan creation (plans.ts savePlan). The user
  sees title + lead + brief disclosure, then a collapsed "Learn more"
  drop-down, then two equal-weight actions: "No thanks" and "Sign up at
  Archera →" (opens archera.ai in a new tab).
- Merge the previously separate "What is Archera Insurance?" (Page A)
  and "How the integration works" (Page B) overlays into a single
  education page (buildArcheraPage). Both ARCHERA_PAGE_A_PATH and
  ARCHERA_PAGE_B_PATH still deep-link to the merged page (PAGE_B kept
  for backward compatibility with already-delivered email links).
- "Learn more" is a native <details>/<summary> that expands the full
  education content inline within the offer modal. Summary text on the
  left, chevron on the right (rotates on open). Body inherits .archera-
  page-inner typography for visual parity with the standalone page.
- Education content tightened: dedupe the lead-paragraph + "Why is CUDly
  telling me about this?" overlap, drop the four-bullet Disclaimers
  section in favour of a single closing "Full disclosure:" paragraph
  that absorbs the legal/scope points. Drop the "All Upfront" qualifier
  from "When it makes sense" and reframe the second use case from
  "finance team requires a safety net" to user-perspective "you want to
  be covered in case your usage drops".
- "How it works" intro now explicitly states the 7-day enrollment
  window from each purchase.
- Premium note clarifies the insurance fee is paid to Archera and the
  cloud commitment bought through CUDly is unaffected (same price,
  same billing) — replaces the earlier sharing-of-savings framing.
- Add aria-haspopup="dialog" + aria-expanded toggling on the (now-
  removed) Columns popover trigger to expose dialog state to assistive
  tech. (No-op for the removed inline CTAs; this change is the modal's
  a11y surface.)
- Drop the inline scripts' em-dashes from JSDoc + user-facing strings.

Tests:
- archera.test.ts rewritten: drops renderArcheraCTA + page A/B + plan-
  modal-CTA + purchase-modal-CTA describe blocks (those surfaces are
  gone). Adds openArcheraOfferModal describes covering visibility,
  context-dependent headlines, signup link attributes, No-thanks +
  backdrop + ESC close, Learn-more collapsible expand, re-open replace
  semantics. handleArcheraDeeplink test now verifies both legacy and
  current URL paths open the same merged page. 1605 frontend tests
  pass.

* feat(email): inline expanded Archera section in approval + confirmation

Replaces the 4-line "Learn more: <URL>" Archera block in the purchase-
confirmation, purchase-approval-request (plain text), and approval-
request (HTML) emails with the full education content inlined into
the message body — so recipients don't need to click through to the
dashboard to read what the option is, when it makes sense, and how
it works.

Structure mirrors the in-app offer modal's expanded "Learn more"
content:
  - Header + 7-day enrollment window note
  - One-paragraph lead (covers the gap + non-gating)
  - "When it makes sense" (2 use cases)
  - "How it works" with the 3-step onboarding flow
  - Premium clarification (insurance fee is separate from the cloud
    commitment; same price, same billing)
  - Full-disclosure paragraph (sponsorship + scope + Archera-owns-terms)
  - Sign up CTA + permalink

Always-expanded (no <details>/<summary>): Outlook desktop, Outlook.com,
and mobile Outlook strip those, so the safest is a flat inline body.
Apple Mail, iOS Mail, and Gmail web render the styled HTML naturally;
all other clients fall back to the plain-text version.

The original {{.ArcheraEducationURL}} is kept as a "Permalink:" at the
bottom of each block — preserves the existing tests' URL assertions
and gives a fallback for forwarded emails, bookmarks, or clients that
mangle the inlined HTML. 310 Go tests still pass.

* fix(frontend/archera): cleanup leaked listener + add focus trap to dialog

Address two CR findings on PR #336:

1. **Stale keydown listener leak**: openArcheraOfferModal added a
   document-level keydown handler (ESC closes) and stored its cleanup in
   `_offerModalCleanup` on every call, but didn't first invoke the prior
   cleanup. A re-open without an explicit close in between leaked the
   previous handler. Fix: call `_offerModalCleanup()` (if set) at the top
   of openArcheraOfferModal before attaching the new handler. Idempotent
   — no-op when no prior open is in flight.

2. **No focus trap on the dialog panel**: the panel declares
   `role="dialog" aria-modal="true"` but Tab / Shift+Tab let keyboard
   users escape into the background app. That's an a11y regression for
   an "aria-modal=true" element. Fix: attach a panel-level keydown
   handler that intercepts Tab — Tab on last focusable wraps to first,
   Shift+Tab on first wraps to last. Focusables are re-queried on each
   Tab press (not cached at modal-build time) so the Tab traversal
   picks up the additional focusables that the <details> drop-down
   expands when opened. Trap handler is torn down alongside the ESC
   handler by the merged `_offerModalCleanup` closure.

Tests in archera.test.ts:
- `re-opening tears down the previous open's ESC listener (no leak)`
  spies on document.addEventListener / removeEventListener and asserts
  the second open both adds AND removes one keydown handler.
- `traps Tab focus inside the panel (Tab from last → first)`
  focuses the last focusable, dispatches Tab, asserts focus wraps to
  the first and the event was preventDefaulted.
- `traps Shift+Tab focus inside the panel (Shift+Tab from first → last)`
  the mirror case.

1608 frontend tests pass (3 new).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/feat New capability urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant