<memvid-mind-context>
# 🧠 Claude Mind Active

📁 Project: **eks-cluster-applications**
💾 Memory: `.claude/mind.mv2` (79 KB)

**Commands:**
- `/mind:search <query>` - Search memories
- `/mind:ask <question>` - Ask your memory
- `/mind:recent` - View timeline
- `/mind:stats` - View statistics

_Memories are captured automatically from your tool use._
</memvid-mind-context>


<EXTREMELY_IMPORTANT>
You have superpowers.

**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**

---
name: using-superpowers
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
---

<SUBAGENT-STOP>
If you were dispatched as a subagent to execute a specific task, skip this skill.
</SUBAGENT-STOP>

<EXTREMELY-IMPORTANT>
If you think there is even a 1% chance a skill might apply to what you are doing, you ABSOLUTELY MUST invoke the skill.

IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.

This is not negotiable. This is not optional. You cannot rationalize your way out of this.
</EXTREMELY-IMPORTANT>

## Instruction Priority

Superpowers skills override default system prompt behavior, but **user instructions always take precedence**:

1. **User's explicit instructions** (CLAUDE.md, GEMINI.md, AGENTS.md, direct requests) — highest priority
2. **Superpowers skills** — override default system behavior where they conflict
3. **Default system prompt** — lowest priority

If CLAUDE.md, GEMINI.md, or AGENTS.md says "don't use TDD" and a skill says "always use TDD," follow the user's instructions. The user is in control.

## How to Access Skills

**In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files.

**In Copilot CLI:** Use the `skill` tool. Skills are auto-discovered from installed plugins. The `skill` tool works the same as Claude Code's `Skill` tool.

**In Gemini CLI:** Skills activate via the `activate_skill` tool. Gemini loads skill metadata at session start and activates the full content on demand.

**In other environments:** Check your platform's documentation for how skills are loaded.

## Platform Adaptation

Skills use Claude Code tool names. Non-CC platforms: see `references/copilot-tools.md` (Copilot CLI), `references/codex-tools.md` (Codex) for tool equivalents. Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.

# Using Skills

## The Rule

**Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means that you should invoke the skill to check. If an invoked skill turns out to be wrong for the situation, you don't need to use it.

```dot
digraph skill_flow {
    "User message received" [shape=doublecircle];
    "About to EnterPlanMode?" [shape=doublecircle];
    "Already brainstormed?" [shape=diamond];
    "Invoke brainstorming skill" [shape=box];
    "Might any skill apply?" [shape=diamond];
    "Invoke Skill tool" [shape=box];
    "Announce: 'Using [skill] to [purpose]'" [shape=box];
    "Has checklist?" [shape=diamond];
    "Create TodoWrite todo per item" [shape=box];
    "Follow skill exactly" [shape=box];
    "Respond (including clarifications)" [shape=doublecircle];

    "About to EnterPlanMode?" -> "Already brainstormed?";
    "Already brainstormed?" -> "Invoke brainstorming skill" [label="no"];
    "Already brainstormed?" -> "Might any skill apply?" [label="yes"];
    "Invoke brainstorming skill" -> "Might any skill apply?";

    "User message received" -> "Might any skill apply?";
    "Might any skill apply?" -> "Invoke Skill tool" [label="yes, even 1%"];
    "Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];
    "Invoke Skill tool" -> "Announce: 'Using [skill] to [purpose]'";
    "Announce: 'Using [skill] to [purpose]'" -> "Has checklist?";
    "Has checklist?" -> "Create TodoWrite todo per item" [label="yes"];
    "Has checklist?" -> "Follow skill exactly" [label="no"];
    "Create TodoWrite todo per item" -> "Follow skill exactly";
}
```

## Red Flags

These thoughts mean STOP—you're rationalizing:

| Thought | Reality |
|---------|---------|
| "This is just a simple question" | Questions are tasks. Check for skills. |
| "I need more context first" | Skill check comes BEFORE clarifying questions. |
| "Let me explore the codebase first" | Skills tell you HOW to explore. Check first. |
| "I can check git/files quickly" | Files lack conversation context. Check for skills. |
| "Let me gather information first" | Skills tell you HOW to gather information. |
| "This doesn't need a formal skill" | If a skill exists, use it. |
| "I remember this skill" | Skills evolve. Read current version. |
| "This doesn't count as a task" | Action = task. Check for skills. |
| "The skill is overkill" | Simple things become complex. Use it. |
| "I'll just do this one thing first" | Check BEFORE doing anything. |
| "This feels productive" | Undisciplined action wastes time. Skills prevent this. |
| "I know what that means" | Knowing the concept ≠ using the skill. Invoke it. |

## Skill Priority

When multiple skills could apply, use this order:

1. **Process skills first** (brainstorming, debugging) - these determine HOW to approach the task
2. **Implementation skills second** (frontend-design, mcp-builder) - these guide execution

"Let's build X" → brainstorming first, then implementation skills.
"Fix this bug" → debugging first, then domain-specific skills.

## Skill Types

**Rigid** (TDD, debugging): Follow exactly. Don't adapt away discipline.

**Flexible** (patterns): Adapt principles to context.

The skill itself tells you which.

## User Instructions

Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows.

</EXTREMELY_IMPORTANT>


<memvid-mind-context>
# 🧠 Claude Mind Active

📁 Project: **eks-cluster-applications**
💾 Memory: `.claude/mind.mv2` (79 KB)

**Commands:**
- `/mind:search <query>` - Search memories
- `/mind:ask <question>` - Ask your memory
- `/mind:recent` - View timeline
- `/mind:stats` - View statistics

_Memories are captured automatically from your tool use._
</memvid-mind-context>

<EXTREMELY_IMPORTANT>
You have superpowers.

**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**

---
name: using-superpowers
description: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
---

<SUBAGENT-STOP>
If you were dispatched as a subagent to execute a specific task, skip this skill.
</SUBAGENT-STOP>

<EXTREMELY-IMPORTANT>
If you think there is even a 1% chance a skill might apply to what you are doing, you ABSOLUTELY MUST invoke the skill.

IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.

This is not negotiable. This is not optional. You cannot rationalize your way out of this.
</EXTREMELY-IMPORTANT>

## Instruction Priority

Superpowers skills override default system prompt behavior, but **user instructions always take precedence**:

1. **User's explicit instructions** (CLAUDE.md, GEMINI.md, AGENTS.md, direct requests) — highest priority
2. **Superpowers skills** — override default system behavior where they conflict
3. **Default system prompt** — lowest priority

If CLAUDE.md, GEMINI.md, or AGENTS.md says "don't use TDD" and a skill says "always use TDD," follow the user's instructions. The user is in control.

## How to Access Skills

**In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files.

**In Copilot CLI:** Use the `skill` tool. Skills are auto-discovered from installed plugins. The `skill` tool works the same as Claude Code's `Skill` tool.

**In Gemini CLI:** Skills activate via the `activate_skill` tool. Gemini loads skill metadata at session start and activates the full content on demand.

**In other environments:** Check your platform's documentation for how skills are loaded.

## Platform Adaptation

Skills use Claude Code tool names. Non-CC platforms: see `references/copilot-tools.md` (Copilot CLI), `references/codex-tools.md` (Codex) for tool equivalents. Gemini CLI users get the tool mapping loaded automatically via GEMINI.md.

# Using Skills

## The Rule

**Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means that you should invoke the skill to check. If an invoked skill turns out to be wrong for the situation, you don't need to use it.

```dot
digraph skill_flow {
    "User message received" [shape=doublecircle];
    "About to EnterPlanMode?" [shape=doublecircle];
    "Already brainstormed?" [shape=diamond];
    "Invoke brainstorming skill" [shape=box];
    "Might any skill apply?" [shape=diamond];
    "Invoke Skill tool" [shape=box];
    "Announce: 'Using [skill] to [purpose]'" [shape=box];
    "Has checklist?" [shape=diamond];
    "Create TodoWrite todo per item" [shape=box];
    "Follow skill exactly" [shape=box];
    "Respond (including clarifications)" [shape=doublecircle];

    "About to EnterPlanMode?" -> "Already brainstormed?";
    "Already brainstormed?" -> "Invoke brainstorming skill" [label="no"];
    "Already brainstormed?" -> "Might any skill apply?" [label="yes"];
    "Invoke brainstorming skill" -> "Might any skill apply?";

    "User message received" -> "Might any skill apply?";
    "Might any skill apply?" -> "Invoke Skill tool" [label="yes, even 1%"];
    "Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];
    "Invoke Skill tool" -> "Announce: 'Using [skill] to [purpose]'";
    "Announce: 'Using [skill] to [purpose]'" -> "Has checklist?";
    "Has checklist?" -> "Create TodoWrite todo per item" [label="yes"];
    "Has checklist?" -> "Follow skill exactly" [label="no"];
    "Create TodoWrite todo per item" -> "Follow skill exactly";
}
```

## Red Flags

These thoughts mean STOP—you're rationalizing:

| Thought | Reality |
|---------|---------|
| "This is just a simple question" | Questions are tasks. Check for skills. |
| "I need more context first" | Skill check comes BEFORE clarifying questions. |
| "Let me explore the codebase first" | Skills tell you HOW to explore. Check first. |
| "I can check git/files quickly" | Files lack conversation context. Check for skills. |
| "Let me gather information first" | Skills tell you HOW to gather information. |
| "This doesn't need a formal skill" | If a skill exists, use it. |
| "I remember this skill" | Skills evolve. Read current version. |
| "This doesn't count as a task" | Action = task. Check for skills. |
| "The skill is overkill" | Simple things become complex. Use it. |
| "I'll just do this one thing first" | Check BEFORE doing anything. |
| "This feels productive" | Undisciplined action wastes time. Skills prevent this. |
| "I know what that means" | Knowing the concept ≠ using the skill. Invoke it. |

## Skill Priority

When multiple skills could apply, use this order:

1. **Process skills first** (brainstorming, debugging) - these determine HOW to approach the task
2. **Implementation skills second** (frontend-design, mcp-builder) - these guide execution

"Let's build X" → brainstorming first, then implementation skills.
"Fix this bug" → debugging first, then domain-specific skills.

## Skill Types

**Rigid** (TDD, debugging): Follow exactly. Don't adapt away discipline.

**Flexible** (patterns): Adapt principles to context.

The skill itself tells you which.

## User Instructions

Instructions say WHAT, not HOW. "Add X" or "Fix Y" doesn't mean skip workflows.

</EXTREMELY_IMPORTANT>


<command-message>obsidian-load</command-message>
<command-name>/obsidian-load</command-name>


Load context from Obsidian notes in the `Claude/` directory into the current conversation.

## Behavior

1. Derive the project name from the current working directory (use the repo or app folder name).
2. List files in `Claude/<project-name>/` using `mcp__obsidian__list_vault_files`.
3. If `` is provided, treat it as a search query and use `mcp__obsidian__search_vault_smart` to find relevant notes across the entire `Claude/` directory instead of listing by project.
4. Read the most relevant notes:
   - Without args: read the 3 most recent notes for the current project (by filename date prefix).
   - With args: read the top 3 search results.
   - Use `mcp__obsidian__get_vault_file` to fetch each note's content.
5. Synthesize what you loaded into a brief summary for the user, covering:
   - What sessions were found and when they occurred
   - Key decisions or context that is now loaded
   - Any open questions or next steps from prior sessions that are still relevant
6. Explicitly tell the user: "I've loaded this context and will use it going forward in this conversation."

## Notes

- If no notes exist for the current project yet, say so and suggest running `/obsidian-save` at the end of a session.
- Do not dump raw note content at the user — synthesize and summarize it.
- If notes contain contradictory information, flag it and ask which is current.


## claude.ai Context7
Use this server to fetch current documentation whenever the user asks about a library, framework, SDK, API, CLI tool, or cloud service -- even well-known ones like React, Next.js, Prisma, Express, Tailwind, Django, or Spring Boot. This includes API syntax, configuration, version migration, library-specific debugging, setup instructions, and CLI tool usage. Use even when you think you know the answer -- your training data may not reflect recent changes. Prefer this over web search for library docs.

Do not use for: refactoring, writing scripts from scratch, debugging business logic, code review, or general programming concepts.


- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled remote agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring remote agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching. Also handles migrating existing Claude API code between Claude model versions (4.5 → 4.6, 4.6 → 4.7, retired-model replacements).
TRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.
SKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.
- obsidian-load: Load context from Obsidian notes in the `Claude/` directory into the current conversation.
- obsidian-save: Save current conversation context to an Obsidian note under the `Claude/` directory.
- mind:stats: Show memory statistics and storage information
- mind:search: Search memories for specific content or patterns
- mind:ask: Ask questions about memories and get context-aware answers
- mind:recent: Show recent memories and activity timeline
- superpowers:using-git-worktrees: Use when starting feature work that needs isolation from current workspace or before executing implementation plans - ensures an isolated workspace exists via native tools or git worktree fallback
- superpowers:test-driven-development: Use when implementing any feature or bugfix, before writing implementation code
- superpowers:systematic-debugging: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
- superpowers:using-superpowers: Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions
- superpowers:requesting-code-review: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
- superpowers:dispatching-parallel-agents: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
- superpowers:receiving-code-review: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
- superpowers:finishing-a-development-branch: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
- superpowers:writing-plans: Use when you have a spec or requirements for a multi-step task, before touching code
- superpowers:executing-plans: Use when you have a written implementation plan to execute in a separate session with review checkpoints
- superpowers:brainstorming: You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.
- superpowers:verification-before-completion: Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
- superpowers:writing-skills: Use when creating new skills, editing existing skills, or verifying skills work before deployment
- superpowers:subagent-driven-development: Use when executing implementation plans with independent tasks in the current session
- mind:memory: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- mind:mind: Claude Mind - Search and manage Claude's persistent memory stored in a single portable .mv2 file
- init: Initialize a new CLAUDE.md file with codebase documentation
- review: Review a pull request
- security-review: Complete a security review of the pending changes on the current branch


Launching skill: obsidian-load


Loading the Obsidian MCP tools and listing notes for this project.


ToolSearch

Question: select:mcp__obsidian__list_vault_files,mcp__obsidian__get_vault_file,mcp__obsidian__search_vault_smart


Question: select:mcp__obsidian__list_vault_files,mcp__obsidian__get_vault_file,mcp__obsidian__search_vault_smart


Error: MCP error -32603: MCP error -32603: GET /vault/Claude/eks-cluster-applications/ 404: {
  "message": "Not Found",
  "errorCode": 40400
}


IAC/ obsidian/ ruth/ steve-briggs/ themes/


Found two recent notes in `Claude/IAC/`. Loading them.


---
date: 2026-05-05
project: IAC
tags: [claude-context]
---

# Vector deploy fixes + netpol additions

## Summary

Post-implementation iteration session. Fixed three deployment-blocking issues that surfaced once Sir Charles started actually deploying the Vector module: helm provider v2/v3 syntax mismatch, a VRL E620 warning from over-narrowed `object!()` calls, and missing CiliumNetworkPolicy coverage for the new vector→Datadog and otel-collector→vector flows. Capped with an ad-hoc test methodology for verifying redaction in a live cluster.

Companion note: `2026-05-05-vector-redaction-pipeline.md` — the original design + implementation session.

## Context

**Pipeline state (post-rollout):** `apps → otel-collector → vector → Datadog`. Vector module deployed locally at `/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/`. otel-collector module bumped (work-in-progress on `otel-batch-size` branch ahead of v1.0.5; needs Sir Charles to handle the rebase to a clean v1.1.0 release branch). Consumer wired in `eks-cluster-applications/vector.tf`.

**Deployment environment provider matrix:**
- Sir Charles's `eks-cluster-applications` consumer pins kubernetes/helm providers to v2.x (kubernetes provider via lockfile, helm provider v2.17.0).
- The implementer subagents had v3.1.0/v3.1.1 resolved in their sandboxes (no lockfile, `>= 2.10` constraint allows v3).
- Result: subagent-authored code used v3 syntax (`set = [{}]` list, `kubernetes = { ... }` equals-block) which broke v2-resolution at deploy time.

**Vector 0.55 VRL stricture:** the compiler narrows types after `if is_object(x)` guards, so subsequently calling `object!(x)` (the abort-on-fail variant) is rejected with E620 — the abort path can never fire. Same for `string!()` on already-string-typed locals.

## Decisions

**1. Use helm v2 syntax in the vector module.** The consumer dictates the resolved version. Reverted `vector/main.tf` `set = [{...}]` → `set { name = ..., value = ... }`. Also fixed `vector/examples/basic/versions.tf` provider helm block (`kubernetes = { ... }` → `kubernetes { ... }`).

**Did NOT tighten `versions.tf` constraint** to `~> 2.10`. Leaving `>= 2.10` keeps the door open for future v3 callers; the syntax fixes work in v2 only, but if Immuta ever upgrades to helm v3 we'll need to revisit syntax anyway. Loose constraint = easier future migration.

**2. Drop the unnecessary `object!()` wrappers from VRL.** Pattern was:
```vrl
if is_object(.attributes) {
  attrs, _err = object(.attributes)
  if is_object(attrs) {
    .attributes = map_values(object!(attrs)) -> |v| { ... }
  }
}
```

The `object!(attrs)` is redundant — after the inner `is_object(attrs)` guard, attrs is narrowed. Just pass `attrs` directly: `map_values(attrs) -> |v| { ... }`.

Applied to all 5 instances in `templates/values.yaml.tpl` and matching test programs (`tests/vrl/redact_emails_*/program.vrl`). All 4 VRL tests still pass.

**3. Explicit egress rule for otel-collector → vector, additive only.**

Added a fourth egress rule to `otel-collector/netpol.tf` selecting Vector pods explicitly:
```hcl
{
  toEndpoints = [{
    matchLabels = {
      "k8s:io.kubernetes.pod.namespace" = "vector"
      "app.kubernetes.io/name"          = "vector"
    }
  }]
  toPorts = [{ ports = [{ port = "4317" }] }]
}
```

**Did NOT remove the existing rules.** The pre-existing `toEntities = ["cluster"]` egress (no port restriction) already permits this traffic; the new rule is defense-in-depth. The `toEntities = ["world"]` on port 443 (formerly direct-to-Datadog HTTPS) is dead weight in downstream mode but stays put — ripping it would couple the netpol to the downstream-mode flag and complicate rollback.

**4. Mirror otel-collector's netpol pattern for the new vector module netpol.**

Vector module had no netpol. Created `vector/netpol.tf` declaring `local.netpols` and a `module "netpol"` call in `vector/main.tf` pointing at the shared `internal-modules/kubernetes/security/network-policy.git?ref=v1.1.0` module. Same call signature as otel-collector — `name = "vector-netpol"`, `namespace = var.namespace`, `network_policies = local.netpols`, depends_on the namespace resource.

Vector netpol surface:
- **Ingress**: `cluster` on 4317/4318 (OTLP from any pod incl. otel-collector); `cluster` on 8686 (self-metrics scrape — Prometheus operator, Datadog Agent OpenMetrics, etc.); `host` on 4317/4318/8686 (kubelet probes); `kube-apiserver` (webhook compatibility, mirroring otel-collector pattern).
- **Egress**: `world:443` (Datadog SaaS sinks); `kube-apiserver`; `cluster` (DNS via CoreDNS + intra-cluster discovery).

**5. Skipped ESO egress from Vector pods.** External Secrets Operator writes the K8s Secret out-of-band; Vector pods just env-mount the resulting Secret. No live ESO traffic from Vector to surface in netpol.

## Task State

**Completed in this session:**

- `vector/main.tf` line 84-93: helm v3 list syntax → v2 block syntax for `set` directives.
- `vector/examples/basic/versions.tf` line 14: helm v3 `kubernetes = { ... }` → v2 `kubernetes { ... }` block.
- `vector/templates/values.yaml.tpl`: 5 instances of `map_values(object!(attrs))` and `map_values(object!(rattrs))` → unwrapped form. Removes the E620 compile warnings at deploy time.
- `vector/tests/vrl/redact_emails_logs/program.vrl`, `redact_emails_traces/program.vrl`, `redact_emails_metrics/program.vrl`: same 5 unwrappings, kept in sync with template. All 4 VRL tests still pass.
- `otel-collector/netpol.tf`: added 4th egress rule for explicit otel→vector permission on port 4317. `terraform validate` clean.
- `vector/netpol.tf` (new): declared `local.netpols` map for Vector's CiliumNetworkPolicy with ingress/egress as documented above.
- `vector/main.tf`: added `module "netpol"` call wiring the netpol locals into the shared network-policy module. depends_on the namespace.
- `vector/.terraform.lock.hcl`: regenerated via `terraform init -upgrade`. Helm provider now locked at `v2.17.0` (the network-policy module's transitive `~> 2.0` constraint forced this). `terraform validate` clean.

**Memory file written earlier (still relevant):** `feedback_platform_components_use_terraform.md` in `~/.claude/projects/-Users-steve-briggs-projects-IAC/memory/`.

**Not yet done (still Sir Charles's responsibility, unchanged from prior note):**
- Tag `vector v1.0.0` in GitLab.
- Tag `otel-collector v1.1.0` (after rebase decision on the `otel-batch-size` branch perf-tuning entanglement).
- Open MRs for all three repos.
- Run `terraform plan` against a real cluster instance directory (Task C3, deferred).

## Open Questions

1. **Real cluster log path.** Spec confirmed apps→OTel→Datadog is the funnel, but it's unclear what component tails container stdout and forwards via OTLP to otel-collector. Could be Datadog Agent in OTLP-forwarding mode, a separate filelog-receiver DaemonSet, or app SDKs emitting OTLP. Doesn't affect redaction correctness (anything that reaches otel-collector gets redacted) but matters for understanding test coverage during rollout.

2. **The `set` syntax fix is one-way.** If Immuta ever upgrades helm provider to v3, the v2 block syntax in `vector/main.tf` will break with the inverse error. Document in the module README's "Compatibility" section as a known limitation pending v3 migration.

3. **`world:443` egress narrowing.** Both otel-collector (now dead-weight in downstream mode) and vector use generic `world:443` for Datadog egress. Could tighten to `toFQDNs = [*.datadoghq.com, *.us3.datadoghq.com, ...]` if Cilium DNS visibility is configured cluster-wide. Tightening hooks: ask compliance if/when defense-in-depth at FQDN level matters.

4. **otel-collector egress catch-all.** The pre-existing `toEntities = ["cluster"]` egress with no port restriction is broad. If Immuta later tightens this, ensure the explicit vector egress rule we added survives the cleanup — it's the only thing then permitting otel→vector traffic.

## Next Steps

**Verifying redaction works end-to-end (ad-hoc method documented to user):**

1. Find the otel-collector OTLP/HTTP service endpoint:
   ```bash
   kubectl get svc -A | grep -i otel
   ```
   Default DNS likely: `otel-collector-lb.<namespace>.svc.cluster.local:4318`.

2. Send three OTLP/HTTP test logs from a one-shot curl pod with a UUID marker:
   - **Email redact**: body `e2e-<UUID>: contact alice@example.com for help` — expect Datadog log with `[REDACTED-EMAIL]` substitution.
   - **Audit drop A**: body `Audit - test-<UUID>` — expect ZERO Datadog results.
   - **Audit drop B**: body `{"level":"audit","event":"test-<UUID>"}` — expect ZERO results.

3. Watch Vector's discard counter during the test:
   ```bash
   kubectl exec -n vector deploy/vector -- curl -s http://localhost:8686/metrics | grep -E "vector_component_(discarded|sent)_events_total"
   ```
   Audit-drop events should tick `discarded_events_total{component_id="drop_audit"}` up; `sent_events_total{component_id="datadog_logs"}` should NOT increase for the dropped messages.

4. **Vector-in-isolation test** (debugging only) — POST OTLP directly to `vector.vector.svc.cluster.local:4318` to bisect "is it OTel-side or Vector-side" if the e2e tests fail.

**Operational follow-ups (carried over):**

5. Author runbook entries: Vector pod debug, queue-full triage, key rotation behavior under reloader, rollback to direct-Datadog (set `vector_enabled = false`).
6. Wire Datadog monitors per spec §6.3 — particularly `vector_component_errors_total{sink=~"datadog_.*"}` rate alerting.
7. Soak the rollout in `mgmt` for 3-7 days before promoting to `cps`/`saas` (per spec §7.4).
8. Decommission `kubernetes/applications/datadog-processing-rules/` after one quarter clean-running in prod.

**Future hardening (post-v1):**

9. Add `output "cluster_secret_store_name"` to external-secrets internal-module; replace `var.vector_external_secrets_cluster_store_name` workaround.
10. Tighten `toEntities = ["world"]` egress to `toFQDNs` for Datadog if compliance asks.
11. Replace `data "external" "vector_validate"` brittle pattern with `lifecycle { precondition }` block.


---
date: 2026-05-05
project: IAC
tags:
  - claude-context
  - "#iac"
  - "#vector"
  - "#datadog"
  - "#observability"
---

# Vector + OpenTelemetry Redaction Pipeline

## Summary

Designed and implemented a Vector-based redaction layer that sits between the existing OTel Collector and Datadog on EKS clusters. The new pipeline replicates today's Datadog Agent `exclude_at_match` audit-log filtering and adds email redaction across logs, traces, and metrics. Spanned three repos: a brand-new `vector` Terraform internal-module, a backwards-compatible bump of the `otel-collector` internal-module (v1.0.5 → v1.1.0), and consumer wiring in `eks-cluster-applications`.

## Context

**Existing telemetry shape:** `apps → otel-lb → otel-collector → Datadog`. OTel Collector is terraform-managed (helm provider). Cluster types `mgmt`/`cps`/`saas` have it on by default; `infra`/`dev` off.

**Existing redaction (being superseded):** `kubernetes/applications/datadog-processing-rules/{prod,staging}/rules.json` — both files identical, two `exclude_at_match` patterns dropping Immuta audit log lines. Datadog Agent processing rules format. `apm.json` empty.

**Sibling app deployment patterns:** Flux-based (license-manager, crow, saas-tenant-manager) use a 4-file pattern: `kustomization.yaml`, `oci-repo-bootstrap.yaml`, `oci-repo-services.yaml`, `flux-kustomization.yaml`. OCI artifacts at `registry.ic-ops.net/internal-tooling/...`.

**Key architectural constraint discovered:** The `otel-collector` internal-module builds helm values **inline via `yamlencode()`** in `otelconfig.tf` and `lbconfig.tf` — NOT via `templatefile()`. This invalidated the plan's "wrap in `%{ if ~}` directive" approach for Phase B; had to use HCL conditionals + `jsonencode/jsondecode` ternary trick (Terraform 1.9+ structural type-checks ternary branches at parse time).

**OTel two-tier deployment:** otel-collector-lb (Deployment) load-balances OTLP to otel-collector-processor (StatefulSet). Only the processor tier exports to Datadog. The LB tier is unaffected by Phase B changes.

**`datadog/connector` for trace-to-metrics:** OTel processor pipeline uses Datadog's APM connector to derive metrics from spans. Kept as-is when swapping exporters — connector is a local computation, not a network call. APM-derived metrics now flow through Vector (datadog_metrics sink) instead of direct, may have subtle DD UI differences. Documented as known limitation.

## Decisions

**1. Terraform-deployed Vector, NOT Flux-deployed.**

User initially said "use Flux" but pushed back when I asked for the deployment medium tradeoffs. Honest comparison: OTel itself is in Terraform; Vector is plumbing not an application; iteration speed for VRL rules wasn't expected to be high enough to justify Flux ceremony; co-locating telemetry components in Terraform keeps the wire diagram in one place.

**Saved as feedback memory** (`feedback_platform_components_use_terraform.md`): for `eks-cluster-applications` plumbing, default to TF/helm provider; Flux is for applications.

**2. Vector behind OTel (Approach A), not in front.**

Pipeline: `apps → otel-lb → otel-collector → vector → Datadog`. Vector receives OTLP, runs VRL transforms, ships via native Datadog sinks. Apps unchanged. OTel terminal exporter swaps from `datadog/exporter` to `otlp` pointing at Vector Service.

**3. Approach (a) on the otel-collector tag dilemma.**

Latest tag in local clone is `v1.0.5`, not `v1.0.7` (spec was wrong). Branch `otel-batch-size` has untagged perf commits ahead of v1.0.5. Decision: base off v1.0.5 for clean diff. Phase B commits ended up on top of `otel-batch-size` regardless — Sir Charles will rebase/cherry-pick for the actual MR.

**4. Skip git operations in this session entirely.**

User runs git himself. Subagents wrote code, ran tests, but didn't `git add`/`git commit`/`git push`. Initial commit attempts hit GPG signing's TTY requirement; pivoted to user-handled-git for all 27 tasks.

**5. Skip Helm values snapshot tests.**

User opted out after seeing the snapshot infrastructure proposal in Task A4. VRL unit tests (functional verification of redaction logic) kept and run real. Template renders verified via ad-hoc `terraform apply -auto-approve` against tempdirs (pure-function `templatefile()` evaluation, no resources).

**6. Removed `downstream_protocol` variable as dead code.**

Phase B's reviewer caught that I declared the variable, wrote a validation block, documented in README and example, but never wired it into the OTLP exporter config. User chose option (a): remove entirely. Documented "HTTP transport is future work" in the `downstream_endpoint` description.

**7. Vector 0.55 VRL idiom adjustments.**

`.path ?? "fallback"` rejected with E651 (path access is infallible). Replaced with `if is_string(.path) { string!(.path) } else { "" }`. For `map_values` lambdas, type narrowing required: `attrs, _err = object(.attributes)` then `map_values(object!(attrs)) -> |v| { if is_string(v) { replace(string!(v), ...) } else { v } }`.

## Task State

**Completed (all 27 plan tasks):**
- **Phase A** (vector module v1.0.0): A1–A15 done. Module fully implemented at `/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector/`. 4 VRL tests pass. `terraform fmt`/`validate` clean at module root and `examples/basic/`.
- **Phase B** (otel-collector v1.1.0 bump): B1–B7 done at `/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/otel-collector/`. Conditional exporter swap, `extraEnvsFrom` gate, ExternalSecret count gate. `examples/downstream/` validates. README created (module had none).
- **Phase C** (eks-cluster-applications consumer): C1–C5 done. `vector.tf` created with 13 vector_* variables. `otel_collector.tf` ref bumped to v1.1.0, `downstream_endpoint` arg, `depends_on = [module.vector]`. README expanded with Vector section, dual-mode data-flow diagram, full inputs table.

**Skipped (intentional):**
- A11: Three-pattern snapshot test (snapshot opt-out).
- B2: Lock baseline snapshot for direct-Datadog mode (snapshot opt-out — backwards-compat verified by ad-hoc plan render instead).
- C3: terraform plan smoke against real cluster instance — deferred. Requires v1.0.0/v1.1.0 tags pushed remotely + cluster auth.

**Memory written:**
- `feedback_platform_components_use_terraform.md` + `MEMORY.md` index in `/Users/steve.briggs/.claude/projects/-Users-steve-briggs-projects-IAC/memory/`.

**Local artifacts written but not committed (Sir Charles handles git):**
- `/Users/steve.briggs/projects/IAC/docs/superpowers/specs/2026-05-05-vector-redaction-design.md` (8-section design spec)
- `/Users/steve.briggs/projects/IAC/docs/superpowers/plans/2026-05-05-vector-redaction-pipeline.md` (27-task implementation plan)
- All three repo edits listed above

## Open Questions

1. **`v1.0.5` vs the entangled `otel-batch-size` branch.** Phase B commits sit on top of perf-tuning commits (`send_batch_max_size = 100 → 75`, `file_storage` extension rename). Decision: bundle perf changes into v1.1.0 release notes, OR re-cut Phase B onto a fresh branch off v1.0.5? Sir Charles to decide before tagging.

2. **`external-secrets` internal-module has no outputs.** Worked around in `vector.tf` with new top-level variable `vector_external_secrets_cluster_store_name`. Long-term fix: add `output "cluster_secret_store_name"` to the external-secrets internal-module, then replace the variable. TODO comment present in `vector.tf`.

3. **ESO `v1` vs `v1beta1` apiVersion** for the ExternalSecret resource in the Vector module. Sibling otel-collector uses `v1`. Adopted `v1` — assumes ESO ≥ 0.9 in the cluster. If older, needs revert to `v1beta1`.

4. **Datadog API key Vault path origin.** Spec called this out as an open implementation question — likely re-use whatever existing variable feeds the OTel module today. To confirm when wiring into actual cluster instance dirs.

5. **Whether `service_monitor_enabled` is the right shipping path** for Vector self-metrics, or whether Datadog Agent's OpenMetrics check / OTel collector annotation-based scraping fits better. Currently opt-in via TF variable, default false.

6. **`data "external" "vector_validate"` is brittle** (mirrors `application_deployment.tf`'s pattern). The `program` field is set to an error string, not an actual executable — fires at plan time when the count condition triggers. Achieves the guard, but a `lifecycle { precondition }` block would be cleaner. Follow-up improvement.

## Next Steps

**Immediate (Sir Charles, git side):**

1. Decide branch strategy for `otel-collector` v1.1.0 — bundle perf or re-cut.
2. Create the GitLab repo `internal-modules/kubernetes/monitoring/vector` if it doesn't exist; push the local repo content.
3. Tag `vector v1.0.0` and `otel-collector v1.1.0`.
4. Open three MRs (vector new repo, otel-collector bump, eks-cluster-applications consumer).

**Once tags are remote:**

5. Run the deferred Task C3 — `terraform plan` against a chosen low-stakes cluster instance dir (e.g. a `dev`-type cluster). Confirm plan shows the new Vector resources and the otel-collector helm release re-render with OTLP exporter.

**Operational rollout (per spec §7.4):**

6. Sandbox/`dev` cluster integration test (full VRL path verification with audit-drop and email-redact UUID smoke tests).
7. One `mgmt` cluster soak (3-7 days, watch `vector_component_errors_total`, queue depth, Datadog ingestion deltas).
8. Remaining `mgmt` + all `cps` clusters.
9. `saas` clusters region-by-region.

**Operational follow-ups (post-rollout, not blocking ship):**

10. Wire Datadog monitors for the alert table in spec §6.3.
11. Author runbook for: Vector pod debug, queue-full triage, key rotation, rollback to direct-Datadog.
12. After Vector clean in prod for one quarter, decommission `kubernetes/applications/datadog-processing-rules/`.
13. Add `output "cluster_secret_store_name"` to external-secrets internal-module and replace the workaround variable.

**Plan-level future work (post-v1):**

14. Expand redaction patterns: SSN, credit cards, tokens, JWT, phone numbers — each as opt-in flag.
15. Field/attribute name allow-deny lists (`vector_drop_attributes`).
16. Service-level allow/deny via `route` transform on `service.name`.
17. Persistent `file_storage` queue on OTel if data-loss budget proves insufficient.
18. Runtime-driven rule sources if redaction surface keeps growing.


Loaded two notes from `Claude/IAC/` covering the same multi-repo Vector rollout, Sir Charles of St Cheese.

**Sessions found:**
- `2026-05-05-vector-redaction-pipeline.md` — original design + 27-task implementation across three repos.
- `2026-05-05-vector-deploy-fixes.md` — follow-up fixing helm v2/v3 syntax mismatch, VRL E620 warnings, and netpol gaps once you started actually deploying.

**Loaded context:**
- Pipeline shape: `apps → otel-lb → otel-collector → vector → Datadog`. Vector is a brand-new internal-module at `internal-modules/kubernetes/monitoring/vector/`; otel-collector bumped to v1.1.0 (conditional exporter swap onto `otel-batch-size` branch); consumer wired here in `vector.tf` + `otel_collector.tf` + README.
- Architectural calls already made: Terraform-deployed (not Flux — that's the saved feedback memory); helm provider pinned v2 syntax in module because consumer locks v2.17.0; VRL `object!()` wrappers stripped after `is_object()` narrowing; otel→vector explicit egress added as defense-in-depth on top of existing `toEntities = ["cluster"]`.
- Audit-log drop + email redaction are functional and unit-tested (4 VRL tests pass).

**Still relevant open questions:**
1. `otel-collector` branch strategy — bundle perf commits into v1.1.0 or re-cut onto v1.0.5 cleanly. Not yet decided.
2. Datadog API key Vault path origin in cluster instance dirs — confirm when wiring.
3. `external-secrets` module needs an output (workaround `vector_external_secrets_cluster_store_name` variable in place with TODO).

**Outstanding next steps for you (the git-side work):**
- Tag `vector v1.0.0` and `otel-collector v1.1.0` and open the three MRs.
- Run deferred Task C3: `terraform plan` against a low-stakes cluster instance dir once tags are remote.
- Run the ad-hoc redaction smoke test (UUID-marker OTLP/HTTP curl + Vector `:8686/metrics` discard counter watch) post-deploy.

**Reality check against current git status:** `vector.tf` still untracked, `otel_collector.tf` and `README.md` modified, `sysdig_agent.tf` modified (new — not from prior sessions). Recent commit `9f456c5` shows flux netpols + DD sampling fix landed since the last note. So the consumer work from 2026-05-05 looks unmerged still on this branch.

I've loaded this context and will use it going forward in this conversation. Anything you want to push on next, or are we tagging modules? (Also: a fresh batch of dino-shaped chicken nuggies wouldn't hurt morale here — far superior to the rounded "rock" extrusions, just saying.)


afplay ~/.claude/sounds/mario/stop/stomp.wav 2>/dev/null || true


Yesterday we worked on the vector terraform module and I was able to deploy that, currently only traces are going from datadog to the open telemetry collector which is fine, what I need to do next is configure datadog to ship logs to vector so we can use the sensitive data redaction in vector. For reference the datadog terraform module that deploys datadog is located at @/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/datadog-agent and the vector module is located at @/Users/steve.briggs/projects/IAC/terraform/internal-modules/kubernetes/monitoring/vector


output.tf
MEMORIES.md
main.tf
processing-rules
README.md
.gitignore
variables.tf
datadog-app.yaml
values.yaml.tftpl
AGENTS.md
.git
CLAUDE.MD
netpols.tf
.idea


outputs.tf
main.tf
versions.tf
tests
.terraform
.claude
README.md
.gitignore
netpol.tf
examples
variables.tf
templates
.gitlab-ci.yml
.terraform.lock.hcl
.idea


Yesterday we worked on the vector terraform module and I was able to deploy that, currently only traces are going from datadog to the open telemetry collector which is fine, what I need to do next is…


Configure Datadog to ship logs to Vector


Before designing the wire-up, let me invoke the brainstorming skill since this is adding new functionality (a new log path through Vector).


Launching skill: superpowers:brainstorming


Base directory for this skill: /Users/steve.briggs/.claude/plugins/cache/claude-plugins-official/superpowers/5.1.0/skills/brainstorming

# Brainstorming Ideas Into Designs

Help turn ideas into fully formed designs and specs through natural collaborative dialogue.

Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.

<HARD-GATE>
Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.
</HARD-GATE>

## Anti-Pattern: "This Is Too Simple To Need A Design"

Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval.

## Checklist

You MUST create a task for each of these items and complete them in order:

1. **Explore project context** — check files, docs, recent commits
2. **Offer visual companion** (if topic will involve visual questions) — this is its own message, not combined with a clarifying question. See the Visual Companion section below.
3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
5. **Present design** — in sections scaled to their complexity, get user approval after each section
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below)
8. **User reviews written spec** — ask user to review the spec file before proceeding
9. **Transition to implementation** — invoke writing-plans skill to create implementation plan

## Process Flow

```dot
digraph brainstorming {
    "Explore project context" [shape=box];
    "Visual questions ahead?" [shape=diamond];
    "Offer Visual Companion\n(own message, no other content)" [shape=box];
    "Ask clarifying questions" [shape=box];
    "Propose 2-3 approaches" [shape=box];
    "Present design sections" [shape=box];
    "User approves design?" [shape=diamond];
    "Write design doc" [shape=box];
    "Spec self-review\n(fix inline)" [shape=box];
    "User reviews spec?" [shape=diamond];
    "Invoke writing-plans skill" [shape=doublecircle];

    "Explore project context" -> "Visual questions ahead?";
    "Visual questions ahead?" -> "Offer Visual Companion\n(own message, no other content)" [label="yes"];
    "Visual questions ahead?" -> "Ask clarifying questions" [label="no"];
    "Offer Visual Companion\n(own message, no other content)" -> "Ask clarifying questions";
    "Ask clarifying questions" -> "Propose 2-3 approaches";
    "Propose 2-3 approaches" -> "Present design sections";
    "Present design sections" -> "User approves design?";
    "User approves design?" -> "Present design sections" [label="no, revise"];
    "User approves design?" -> "Write design doc" [label="yes"];
    "Write design doc" -> "Spec self-review\n(fix inline)";
    "Spec self-review\n(fix inline)" -> "User reviews spec?";
    "User reviews spec?" -> "Write design doc" [label="changes requested"];
    "User reviews spec?" -> "Invoke writing-plans skill" [label="approved"];
}
```

**The terminal state is invoking writing-plans.** Do NOT invoke frontend-design, mcp-builder, or any other implementation skill. The ONLY skill you invoke after brainstorming is writing-plans.

## The Process

**Understanding the idea:**

- Check out the current project state first (files, docs, recent commits)
- Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
- If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
- For appropriately-scoped projects, ask questions one at a time to refine the idea
- Prefer multiple choice questions when possible, but open-ended is fine too
- Only one question per message - if a topic needs more exploration, break it into multiple questions
- Focus on understanding: purpose, constraints, success criteria

**Exploring approaches:**

- Propose 2-3 different approaches with trade-offs
- Present options conversationally with your recommendation and reasoning
- Lead with your recommended option and explain why

**Presenting the design:**

- Once you believe you understand what you're building, present the design
- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
- Ask after each section whether it looks right so far
- Cover: architecture, components, data flow, error handling, testing
- Be ready to go back and clarify if something doesn't make sense

**Design for isolation and clarity:**

- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently
- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
- Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much.

**Working in existing codebases:**

- Explore the current structure before proposing changes. Follow existing patterns.
- Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in.
- Don't propose unrelated refactoring. Stay focused on what serves the current goal.

## After the Design

**Documentation:**

- Write the validated design (spec) to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
  - (User preferences for spec location override this default)
- Use elements-of-style:writing-clearly-and-concisely skill if available
- Commit the design document to git

**Spec Self-Review:**
After writing the spec document, look at it with fresh eyes:

1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them.
2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions?
3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition?
4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit.

Fix any issues inline. No need to re-review — just fix and move on.

**User Review Gate:**
After the spec review loop passes, ask the user to review the written spec before proceeding:

> "Spec written and committed to `<path>`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."

Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.

**Implementation:**

- Invoke the writing-plans skill to create a detailed implementation plan
- Do NOT invoke any other skill. writing-plans is the next step.

## Key Principles

- **One question at a time** - Don't overwhelm with multiple questions
- **Multiple choice preferred** - Easier to answer than open-ended when possible
- **YAGNI ruthlessly** - Remove unnecessary features from all designs
- **Explore alternatives** - Always propose 2-3 approaches before settling
- **Incremental validation** - Present design, get approval before moving on
- **Be flexible** - Go back and clarify when something doesn't make sense

## Visual Companion

A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool — not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser.

**Offering the companion:** When you anticipate that upcoming questions will involve visual content (mockups, layouts, diagrams), offer it once for consent:
> "Some of what we're working on might be easier to explain if I can show it to you in a web browser. I can put together mockups, diagrams, comparisons, and other visuals as we go. This feature is still new and can be token-intensive. Want to try it? (Requires opening a local URL)"

**This offer MUST be its own message.** Do not combine it with clarifying questions, context summaries, or any other content. The message should contain ONLY the offer above and nothing else. Wait for the user's response before continuing. If they decline, proceed with text-only brainstorming.

**Per-question decision:** Even after the user accepts, decide FOR EACH QUESTION whether to use the browser or the terminal. The test: **would the user understand this better by seeing it than reading it?**

- **Use the browser** for content that IS visual — mockups, wireframes, layout comparisons, architecture diagrams, side-by-side visual designs
- **Use the terminal** for content that is text — requirements questions, conceptual choices, tradeoff lists, A/B/C/D text options, scope decisions

A question about a UI topic is not automatically a visual question. "What does personality mean in this context?" is a conceptual question — use the terminal. "Which wizard layout works better?" is a visual question — use the browser.

If they agree to the companion, read the detailed guide before proceeding:
`skills/brainstorming/visual-companion.md`


Loading task tracking and exploring the relevant module files in parallel.


Question: select:TaskCreate,TaskUpdate,TaskList


Question: select:TaskCreate,TaskUpdate,TaskList


Bash

List datadog-agent processing-rules directory


1	targetSystem: "linux"
2	registry: public.ecr.aws/datadog
3	datadog:
4	  operator:
5	    enabled: false
6	  apiKeyExistingSecret: ${datadog_api_key_secret_name}
7	  clusterName: ${cluster_name}
8	  securityContext:
9	    seLinuxOptions:
10	      user: "system_u"
11	      role: "system_r"
12	      type: "spc_t"
13	      level: "s0"
14	  logLevel: ${log_level}
15	  containerExclude: "${container_exclude}"
16	  containerExcludeLogs: "${container_exclude_logs}"
17	  logs:
18	    enabled: true
19	    containerCollectAll: true
20	    autoMultiLineDetection: true
21	  criSocketPath: /run/containerd/containerd.sock
22	  dogstatsd:
23	    port: 8125
24	    useHostPort: true
25	    nonLocalTraffic: true
26	  otlp:
27	    receiver:
28	      protocols:
29	        grpc:
30	          enabled: true
31	          endpoint: 0.0.0.0:4317
32	        http:
33	          enabled: true
34	          endpoint: 0.0.0.0:4318
35	  apm:
36	    enabled: ${apm_enabled}
37	    instrumentation:
38	      skipKPITelemetry: true
39	  processAgent:
40	    enabled: false
41	  networkMonitoring:
42	    enabled: false
43	  tags:
44	    - "global_segment:${global_segment}"
45	%{ for tag in tags ~}
46	    - "${tag}"
47	%{ endfor ~}
48	  confd:
49	    journald_conf.yaml: |-
50	      logs:
51	        - type: journald
52	          path: /var/log/journal/
53	          start_position: beginning
54	          source: journald
55	          exclude_units:
56	            - kubelet.service
57	  env:
58	    - name: DD_AUTOCONFIG_INCLUDE_FEATURES
59	      value: "containerd"
60	    - name: DD_ENV
61	      value: ${env}
62	    - name: DD_KUBERNETES_NODE_LABELS_AS_TAGS
63	      value: '{"kubernetes.io/hostname":"node_name","kubernetes.io/os":"node_os","beta.kubernetes.io/instance-type":"node_instance_type","node.kubernetes.io/lifecycle":"node_lifecycle","failure-domain.beta.kubernetes.io/region":"node_region","failure-domain.beta.kubernetes.io/zone":"node_zone"}'
64	    - name: DD_LOGS_CONFIG_PROCESSING_RULES
65	      valueFrom:
66	        configMapKeyRef:
67	          name: rules-config
68	          key: logs
69	    - name: DD_APM_REPLACE_TAGS
70	      valueFrom:
71	        configMapKeyRef:
72	          name: rules-config
73	          key: apm
74	    - name: DD_CLOUD_PROVIDER_METADATA
75	      value: "aws"
76	    - name: CLUSTER_NAME
77	      value: "${cluster_name}"
78	    - name: DD_APM_PROBABILISTIC_SAMPLER_ENABLED
79	      value: true
80	    - name: DD_APM_PROBABILISTIC_SAMPLER_SAMPLING_PERCENTAGE
81	      value: 15
82	clusterAgent:
83	  env:
84	    - name: DD_ENV
85	      value: ${env}
86	    - name: DD_KUBERNETES_NODE_LABELS_AS_TAGS
87	      value: '{"kubernetes.io/hostname":"node_name","kubernetes.io/os":"node_os","beta.kubernetes.io/instance-type":"node_instance_type","node.kubernetes.io/lifecycle":"node_lifecycle","failure-domain.beta.kubernetes.io/region":"node_region","failure-domain.beta.kubernetes.io/zone":"node_zone"}'
88	    - name: DD_LOGS_CONFIG_PROCESSING_RULES
89	      valueFrom:
90	        configMapKeyRef:
91	          name: rules-config
92	          key: logs
93	    - name: DD_APM_REPLACE_TAGS
94	      valueFrom:
95	        configMapKeyRef:
96	          name: rules-config
97	          key: apm
98	    - name: DD_CLOUD_PROVIDER_METADATA
99	      value: "aws"
100	    - name: CLUSTER_NAME
101	      value: "${cluster_name}"
102	%{ if enable_mrds_monitoring ~}
103	    - name: MRDS_HOST
104	      valueFrom:
105	        secretKeyRef:
106	          name: datadog-env
107	          key: MRDS_HOST
108	    - name: MRDS_PORT
109	      valueFrom:
110	        secretKeyRef:
111	          name: datadog-env
112	          key: MRDS_PORT
113	    - name: MRDS_USERNAME
114	      valueFrom:
115	        secretKeyRef:
116	          name: datadog-env
117	          key: MRDS_USERNAME
118	    - name: MRDS_PASSWORD
119	      valueFrom:
120	        secretKeyRef:
121	          name: datadog-env
122	          key: MRDS_PASSWORD
123	    - name: MRDS_REGION
124	      valueFrom:
125	        secretKeyRef:
126	          name: datadog-env
127	          key: MRDS_REGION
128	    - name: MRDS_INSTANCE_NAME
129	      valueFrom:
130	        secretKeyRef:
131	          name: datadog-env
132	          key: MRDS_INSTANCE_NAME
133	%{ endif ~}
134	%{ if enable_nowrds_monitoring ~}
135	    - name: NOWRDS_HOST
136	      valueFrom:
137	        secretKeyRef:
138	          name: datadog-env
139	          key: NOWRDS_HOST
140	    - name: NOWRDS_PORT
141	      valueFrom:
142	        secretKeyRef:
143	          name: datadog-env
144	          key: NOWRDS_PORT
145	    - name: NOWRDS_USERNAME
146	      valueFrom:
147	        secretKeyRef:
148	          name: datadog-env
149	          key: NOWRDS_USERNAME
150	    - name: NOWRDS_PASSWORD
151	      valueFrom:
152	        secretKeyRef:
153	          name: datadog-env
154	          key: NOWRDS_PASSWORD
155	    - name: NOWRDS_INSTANCE_NAME
156	      valueFrom:
157	        secretKeyRef:
158	          name: datadog-env
159	          key: NOWRDS_INSTANCE_NAME
160	%{ endif ~}
161	  confd:
162	%{ if enable_mrds_monitoring || enable_nowrds_monitoring ~}
163	    postgres.yaml: |-
164	      cluster_check: true
165	      init_config:
166	      instances:
167	%{ if enable_mrds_monitoring ~}
168	        - dbm: true
169	          ssl: allow
170	          host: "%%env_MRDS_HOST%%"
171	          port: "%%env_MRDS_PORT%%"
172	          username: "%%env_MRDS_USERNAME%%"
173	          password: "%%env_MRDS_PASSWORD%%"
174	          collect_schemas:
175	            enabled: true
176	          database_autodiscovery:
177	            enabled: true
178	          tags:
179	            - "dbinstanceidentifier:%%env_MRDS_INSTANCE_NAME%%"
180	            - "env:%%env_DD_ENV%%"
181	%{ endif ~}
182	%{ if enable_nowrds_monitoring ~}
183	        - dbm: true
184	          ssl: allow
185	          host: "%%env_NOWRDS_HOST%%"
186	          port: "%%env_NOWRDS_PORT%%"
187	          username: "%%env_NOWRDS_USERNAME%%"
188	          password: "%%env_NOWRDS_PASSWORD%%"
189	          collect_schemas:
190	            enabled: true
191	          database_autodiscovery:
192	            enabled: true
193	%{ if enable_now_obfusicator_options ~}
194	          obfuscator_options:
195	%{ for key, option in now_obfusicator_options ~}
196	            ${key}: ${option}
197	%{ endfor ~}
198	%{ endif ~}
199	          tags:
200	            - "dbinstanceidentifier:%%env_NOWRDS_INSTANCE_NAME%%"
201	            - "env:%%env_DD_ENV%%"
202	%{ endif ~}
203	%{ endif ~}
204	  enabled: true
205	  image:
206	    tag: ${image_tag}
207	  priorityClassName: ${agents_priority_class_name}
208	  tokenExistingSecret: ${cluster_agent_token_secret_name}
209	  admissionController:
210	    enabled: true
211	    configMode: socket
212	agents:
213	  enabled: true
214	  image:
215	    tag: ${image_tag}
216	  priorityClassName: ${agents_priority_class_name}
217	  tolerations:
218	    - operator: Exists
219	  useHostNetwork: true
220	  volumes:
221	    - name: journald
222	      hostPath:
223	        path: "/var/log/journal"
224	    - name: machineid
225	      hostPath:
226	        path: "/etc/machine-id"
227	  volumeMounts:
228	    - name: journald
229	      mountPath: "/var/log/journal"
230	      readOnly: true
231	    - name: machineid
232	      mountPath: "/etc/machine-id"
233	      readOnly: true
234

targetSystem: "linux"
registry: public.ecr.aws/datadog
datadog:
  operator:
    enabled: false
  apiKeyExistingSecret: ${datadog_api_key_secret_name}
  clusterName: ${cluster_name}
  securityContext:
    seLinuxOptions:
      user: "system_u"
      role: "system_r"
      type: "spc_t"
      level: "s0"
  logLevel: ${log_level}
  containerExclude: "${container_exclude}"
  containerExcludeLogs: "${container_exclude_logs}"
  logs:
    enabled: true
    containerCollectAll: true
    autoMultiLineDetection: true
  criSocketPath: /run/containerd/containerd.sock
  dogstatsd:
    port: 8125
    useHostPort: true
    nonLocalTraffic: true
  otlp:
    receiver:
      protocols:
        grpc:
          enabled: true
          endpoint: 0.0.0.0:4317
        http:
          enabled: true
          endpoint: 0.0.0.0:4318
  apm:
    enabled: ${apm_enabled}
    instrumentation:
      skipKPITelemetry: true
  processAgent:
    enabled: false
  networkMonitoring:
    enabled: false
  tags:
    - "global_segment:${global_segment}"
%{ for tag in tags ~}
    - "${tag}"
%{ endfor ~}
  confd:
    journald_conf.yaml: |-
      logs:
        - type: journald
          path: /var/log/journal/
          start_position: beginning
          source: journald
          exclude_units:
            - kubelet.service
  env:
    - name: DD_AUTOCONFIG_INCLUDE_FEATURES
      value: "containerd"
    - name: DD_ENV
      value: ${env}
    - name: DD_KUBERNETES_NODE_LABELS_AS_TAGS
      value: '{"kubernetes.io/hostname":"node_name","kubernetes.io/os":"node_os","beta.kubernetes.io/instance-type":"node_instance_type","node.kubernetes.io/lifecycle":"node_lifecycle","failure-domain.beta.kubernetes.io/region":"node_region","failure-domain.beta.kubernetes.io/zone":"node_zone"}'
    - name: DD_LOGS_CONFIG_PROCESSING_RULES
      valueFrom:
        configMapKeyRef:
          name: rules-config
          key: logs
    - name: DD_APM_REPLACE_TAGS
      valueFrom:
        configMapKeyRef:
          name: rules-config
          key: apm
    - name: DD_CLOUD_PROVIDER_METADATA
      value: "aws"
    - name: CLUSTER_NAME
      value: "${cluster_name}"
    - name: DD_APM_PROBABILISTIC_SAMPLER_ENABLED
      value: true
    - name: DD_APM_PROBABILISTIC_SAMPLER_SAMPLING_PERCENTAGE
      value: 15
clusterAgent:
  env:
    - name: DD_ENV
      value: ${env}
    - name: DD_KUBERNETES_NODE_LABELS_AS_TAGS
      value: '{"kubernetes.io/hostname":"node_name","kubernetes.io/os":"node_os","beta.kubernetes.io/instance-type":"node_instance_type","node.kubernetes.io/lifecycle":"node_lifecycle","failure-domain.beta.kubernetes.io/region":"node_region","failure-domain.beta.kubernetes.io/zone":"node_zone"}'
    - name: DD_LOGS_CONFIG_PROCESSING_RULES
      valueFrom:
        configMapKeyRef:
          name: rules-config
          key: logs
    - name: DD_APM_REPLACE_TAGS
      valueFrom:
        configMapKeyRef:
          name: rules-config
          key: apm
    - name: DD_CLOUD_PROVIDER_METADATA
      value: "aws"
    - name: CLUSTER_NAME
      value: "${cluster_name}"
%{ if enable_mrds_monitoring ~}
    - name: MRDS_HOST
      valueFrom:
        secretKeyRef:
          name: datadog-env
          key: MRDS_HOST
    - name: MRDS_PORT
      valueFrom:
        secretKeyRef:
          name: datadog-env
          key: MRDS_PORT
    - name: MRDS_USERNAME
      valueFrom:
        secretKeyRef:
          name: datadog-env
          key: MRDS_USERNAME
    - name: MRDS_PASSWORD
      valueFrom:
        secretKeyRef:
          name: datadog-env
          key: MRDS_PASSWORD
    - name: MRDS_REGION
      valueFrom:
        secretKeyRef:
          name: datadog-env
          key: MRDS_REGION
    - name: MRDS_INSTANCE_NAME
      valueFrom:
        secretKeyRef:
          name: datadog-env
          key: MRDS_INSTANCE_NAME
%{ endif ~}
%{ if enable_nowrds_monitoring ~}
    - name: NOWRDS_HOST
      valueFrom:
        secretKeyRef:
          name: datadog-env
          key: NOWRDS_HOST
    - name: NOWRDS_PORT
      valueFrom:
        secretKeyRef:
          name: datadog-env
          key: NOWRDS_PORT
    - name: NOWRDS_USERNAME
      valueFrom:
        secretKeyRef:
          name: datadog-env
          key: NOWRDS_USERNAME
    - name: NOWRDS_PASSWORD
      valueFrom:
        secretKeyRef:
          name: datadog-env
          key: NOWRDS_PASSWORD
    - name: NOWRDS_INSTANCE_NAME
      valueFrom:
        secretKeyRef:
          name: datadog-env
          key: NOWRDS_INSTANCE_NAME
%{ endif ~}
  confd:
%{ if enable_mrds_monitoring || enable_nowrds_monitoring ~}
    postgres.yaml: |-
      cluster_check: true
      init_config:
      instances:
%{ if enable_mrds_monitoring ~}
        - dbm: true
          ssl: allow
          host: "%%env_MRDS_HOST%%"
          port: "%%env_MRDS_PORT%%"
          username: "%%env_MRDS_USERNAME%%"
          password: "%%env_MRDS_PASSWORD%%"
          collect_schemas:
            enabled: true
          database_autodiscovery:
            enabled: true
          tags:
            - "dbinstanceidentifier:%%env_MRDS_INSTANCE_NAME%%"
            - "env:%%env_DD_ENV%%"
%{ endif ~}
%{ if enable_nowrds_monitoring ~}
        - dbm: true
          ssl: allow
          host: "%%env_NOWRDS_HOST%%"
          port: "%%env_NOWRDS_PORT%%"
          username: "%%env_NOWRDS_USERNAME%%"
          password: "%%env_NOWRDS_PASSWORD%%"
          collect_schemas:
            enabled: true
          database_autodiscovery:
            enabled: true
%{ if enable_now_obfusicator_options ~}
          obfuscator_options:
%{ for key, option in now_obfusicator_options ~}
            ${key}: ${option}
%{ endfor ~}
%{ endif ~}
          tags:
            - "dbinstanceidentifier:%%env_NOWRDS_INSTANCE_NAME%%"
            - "env:%%env_DD_ENV%%"
%{ endif ~}
%{ endif ~}
  enabled: true
  image:
    tag: ${image_tag}
  priorityClassName: ${agents_priority_class_name}
  tokenExistingSecret: ${cluster_agent_token_secret_name}
  admissionController:
    enabled: true
    configMode: socket
agents:
  enabled: true
  image:
    tag: ${image_tag}
  priorityClassName: ${agents_priority_class_name}
  tolerations:
    - operator: Exists
  useHostNetwork: true
  volumes:
    - name: journald
      hostPath:
        path: "/var/log/journal"
    - name: machineid
      hostPath:
        path: "/etc/machine-id"
  volumeMounts:
    - name: journald
      mountPath: "/var/log/journal"
      readOnly: true
    - name: machineid
      mountPath: "/etc/machine-id"
      readOnly: true


drwxr-xr-x steve.briggs staff 128 B Thu Feb 12 17:09:38 2026 .
drwxr-xr-x steve.briggs staff 128 B Thu Feb 12 17:10:33 2026 prod
drwxr-xr-x steve.briggs staff 128 B Tue Feb 24 16:20:50 2026 staging
drwxr-xr-x steve.briggs staff 512 B Wed May  6 10:56:28 2026 ..


1	variable "name" {
2	  description = "The name of the Helm release."
3	  type        = string
4	  default     = "immuta-dd"
5	}
6
7	variable "namespace" {
8	  description = "The Kubernetes namespace to deploy the Datadog agent into."
9	  type        = string
10	  default     = "datadog"
11	}
12
13	variable "create_namespace" {
14	  description = "Whether to create the namespace. Set to false if the namespace already exists."
15	  type        = bool
16	  default     = true
17	}
18
19	variable "chart_version" {
20	  description = "The version of the Datadog Helm chart to use."
21	  type        = string
22	}
23
24	variable "env" {
25	    description = "The environment that resources are deployed to. i.e. nonprod, prod"
26	    type        = string
27	}
28
29	variable "global_segment" {
30	  description = "The global segment to use for tagging and other purposes."
31	  type        = string
32	}
33
34	variable "image_tag" {
35	    description = "The tag of the Datadog agent image to use."
36	    type        = string
37	}
38
39	variable "cluster_name" {
40	  description = "The name of the Kubernetes cluster, to be used for tagging."
41	  type        = string
42	}
43
44	variable "datadog_tags" {
45	    description = "A map of global tags to apply to all metrics, traces, and logs."
46	    type        = list(string)
47	    default     = []
48	}
49
50	variable "enable_now_obfusicator_options" {
51	    description = "Whether to enable the NOWRDS obfuscator options in the Datadog agent configuration."
52	    type        = bool
53	    default     = false
54	}
55
56	variable "now_obfusicator_options" {
57	    description = "A map of options to configure the NOWRDS obfuscator in the Datadog agent configuration."
58	    type        = map(string)
59	    default     = {
60	      replace_digits = "true"
61	    }
62	}
63
64	variable "datadog_api_key_secret_name" {
65	  description = "The name of the Kubernetes secret holding the Datadog API key."
66	  type        = string
67	  default     = "datadog-api-key"
68	}
69
70	variable "cluster_agent_token_secret_name" {
71	  description = "The name of the Kubernetes secret for the cluster agent token."
72	  type        = string
73	  default     = "datadog-clusteragent-token"
74	}
75
76	variable "log_level" {
77	    description = "The log level for the Datadog agent."
78	    type        = string
79	    default     = "ERROR"
80	}
81
82	variable "apm_enabled" {
83	    description = "Whether to enable APM for the Datadog agent."
84	    type        = bool
85	    default     = true
86	}
87
88	variable "container_exclude" {
89	    description = "A list of container names to exclude from monitoring."
90	    type        = string
91	    default     = "kube_namespace:datadog name:enterprise"
92	}
93
94	variable "container_exclude_logs" {
95	    description = "A list of container names to exclude from log collection."
96	    type        = string
97	    default     = "image:registry.k8s.io/autoscaling/cluster-autoscaler image:registry.ic-ops.net/cache-k8s-registry/autoscaling/cluster-autoscaler"
98	}
99
100	variable "tags" {
101	  description = "A list of global tags to apply to all metrics, traces, and logs."
102	  type        = map(any)
103	  default     = {}
104	}
105
106	variable "agents_priority_class_name" {
107	  description = "The priority class name for the Datadog agents."
108	  type        = string
109	  default     = "cloud-ops-system"
110	}
111
112	variable "enable_nowrds_monitoring" {
113	  description = "Enable monitoring for NOWRDS."
114	  type        = bool
115	  default     = false
116	}
117
118	variable "enable_mrds_monitoring" {
119	  description = "Enable monitoring for MRDS."
120	  type        = bool
121	  default     = false
122	}
123

variable "name" {
  description = "The name of the Helm release."
  type        = string
  default     = "immuta-dd"
}

variable "namespace" {
  description = "The Kubernetes namespace to deploy the Datadog agent into."
  type        = string
  default     = "datadog"
}

variable "create_namespace" {
  description = "Whether to create the namespace. Set to false if the namespace already exists."
  type        = bool
  default     = true
}

variable "chart_version" {
  description = "The version of the Datadog Helm chart to use."
  type        = string
}

variable "env" {
    description = "The environment that resources are deployed to. i.e. nonprod, prod"
    type        = string
}

variable "global_segment" {
  description = "The global segment to use for tagging and other purposes."
  type        = string
}

variable "image_tag" {
    description = "The tag of the Datadog agent image to use."
    type        = string
}

variable "cluster_name" {
  description = "The name of the Kubernetes cluster, to be used for tagging."
  type        = string
}

variable "datadog_tags" {
    description = "A map of global tags to apply to all metrics, traces, and logs."
    type        = list(string)
    default     = []
}

variable "enable_now_obfusicator_options" {
    description = "Whether to enable the NOWRDS obfuscator options in the Datadog agent configuration."
    type        = bool
    default     = false
}

variable "now_obfusicator_options" {
    description = "A map of options to configure the NOWRDS obfuscator in the Datadog agent configuration."
    type        = map(string)
    default     = {
      replace_digits = "true"
    }
}

variable "datadog_api_key_secret_name" {
  description = "The name of the Kubernetes secret holding the Datadog API key."
  type        = string
  default     = "datadog-api-key"
}

variable "cluster_agent_token_secret_name" {
  description = "The name of the Kubernetes secret for the cluster agent token."
  type        = string
  default     = "datadog-clusteragent-token"
}

variable "log_level" {
    description = "The log level for the Datadog agent."
    type        = string
    default     = "ERROR"
}

variable "apm_enabled" {
    description = "Whether to enable APM for the Datadog agent."
    type        = bool
    default     = true
}

variable "container_exclude" {
    description = "A list of container names to exclude from monitoring."
    type        = string
    default     = "kube_namespace:datadog name:enterprise"
}

variable "container_exclude_logs" {
    description = "A list of container names to exclude from log collection."
    type        = string
    default     = "image:registry.k8s.io/autoscaling/cluster-autoscaler image:registry.ic-ops.net/cache-k8s-registry/autoscaling/cluster-autoscaler"
}

variable "tags" {
  description = "A list of global tags to apply to all metrics, traces, and logs."
  type        = map(any)
  default     = {}
}

variable "agents_priority_class_name" {
  description = "The priority class name for the Datadog agents."
  type        = string
  default     = "cloud-ops-system"
}

variable "enable_nowrds_monitoring" {
  description = "Enable monitoring for NOWRDS."
  type        = bool
  default     = false
}

variable "enable_mrds_monitoring" {
  description = "Enable monitoring for MRDS."
  type        = bool
  default     = false
}


1	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2	# Deploys the Datadog Agent to a Kubernetes cluster using the official Helm chart.
3	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5	# ---------------------------------------------------------------------------------------------------------------------
6	# SET TERRAFORM REQUIREMENTS FOR RUNNING THIS MODULE
7	# ---------------------------------------------------------------------------------------------------------------------
8
9	terraform {
10	  required_version = ">= 1.3.0, < 2.0"
11
12	  required_providers {
13	    helm = {
14	      source  = "hashicorp/helm"
15	      version = "~> 2.0"
16	    }
17	    kubernetes = {
18	      source  = "hashicorp/kubernetes"
19	      version = "~> 2.0"
20	    }
21	  }
22	}
23
24	locals {
25	  apm_rules = file("${path.module}/processing-rules/${var.env}/apm.json")
26	  log_rules = file("${path.module}/processing-rules/${var.env}/rules.json")
27	  nowrds_instance = var.enable_nowrds_monitoring ? (<<-EON
28	    - dbm: true
29	      ssl: allow
30	      host: "%%env_NOWRDS_HOST%%"
31	      port: "%%env_NOWRDS_PORT%%"
32	      username: "%%env_NOWRDS_USERNAME%%"
33	      password: "%%env_NOWRDS_PASSWORD%%"
34	      collect_schemas:
35	        enabled: true
36	      database_autodiscovery:
37	        enabled: true
38	      tags:
39	        - "dbinstanceidentifier:%%env_NOWRDS_INSTANCE_NAME%%"
40	        - "env:%%env_DD_ENV%%"
41	  EON
42	  ) : null
43	}
44	# ---------------------------------------------------------------------------------------------------------------------
45	# CREATE A NAMESPACE
46	# ---------------------------------------------------------------------------------------------------------------------
47	resource "kubernetes_namespace" "this" {
48	  count = var.create_namespace ? 1 : 0
49	  metadata {
50	    name = var.namespace
51	  }
52	}
53
54	resource "kubernetes_config_map" "datadog_processing_rules" {
55	  metadata {
56	    name = "rules-config"
57	    namespace = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
58	  }
59	  data = {
60	    //"apm"  = local.apm_rules
61	    "apm" = file("${path.module}/processing-rules/${var.env}/apm.json")
62	    //"logs" = local.log_rules
63	    "logs" = file("${path.module}/processing-rules/${var.env}/rules.json")
64	  }
65	  depends_on = [kubernetes_namespace.this]
66	}
67
68	resource "kubernetes_manifest" "external-secret-dd-api-key" {
69	  manifest = {
70	    apiVersion = "external-secrets.io/v1"
71	    kind       = "ExternalSecret"
72	    metadata = {
73	      name      = "datadog-api-key"
74	      namespace = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
75	    }
76	    spec = {
77	      refreshInterval = "1h0m0s"
78	      secretStoreRef = {
79	        name = "vault"
80	        kind = "ClusterSecretStore"
81	      }
82	      target = {
83	        name           = "datadog-api-key"
84	        creationPolicy = "Owner"
85	      }
86	      data = [{
87	        secretKey = "api-key"
88	        remoteRef = {
89	          key      = "saas/infrastructure/datadog-api-key"
90	          property = "apiKey"
91	        }
92	      }]
93	    }
94	  }
95	}
96
97	resource "kubernetes_manifest" "external-secret-dd-api-token" {
98	  manifest = {
99	    apiVersion = "external-secrets.io/v1"
100	    kind       = "ExternalSecret"
101	    metadata = {
102	      name      = "datadog-clusteragent-token"
103	      namespace = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
104	    }
105	    spec = {
106	      refreshInterval = "1h0m0s"
107	      secretStoreRef = {
108	        name = "vault"
109	        kind = "ClusterSecretStore"
110	      }
111	      target = {
112	        name           = "datadog-clusteragent-token"
113	        creationPolicy = "Owner"
114	      }
115	      data = [{
116	        secretKey = "token"
117	        remoteRef = {
118	          key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
119	          property = "token"
120	        }
121	      }]
122	    }
123	  }
124	}
125
126	resource "kubernetes_manifest" "external-secret" {
127	  count = (var.enable_mrds_monitoring || var.enable_nowrds_monitoring) ? 1 : 0
128
129	  manifest = {
130	    apiVersion = "external-secrets.io/v1"
131	    kind       = "ExternalSecret"
132	    metadata = {
133	      name      = "datadog-env"
134	      namespace = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
135	    }
136	    spec = {
137	      refreshInterval = "1h0m0s"
138	      secretStoreRef = {
139	        name = "vault"
140	        kind = "ClusterSecretStore"
141	      }
142	      target = {
143	        name           = "datadog-env"
144	        creationPolicy = "Owner"
145	      }
146	      data = concat(var.enable_mrds_monitoring ? [
147	        {
148	          secretKey = "MRDS_HOST"
149	          remoteRef = {
150	            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
151	            property = "rds-host"
152	          }
153	        },
154	        {
155	          secretKey = "MRDS_PORT"
156	          remoteRef = {
157	            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
158	            property = "rds-port"
159	          }
160	        },
161	        {
162	          secretKey = "MRDS_USERNAME"
163	          remoteRef = {
164	            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
165	            property = "rds-username"
166	          }
167	        },
168	        {
169	          secretKey = "MRDS_PASSWORD"
170	          remoteRef = {
171	            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
172	            property = "rds-password"
173	          }
174	        },
175	        {
176	          secretKey = "MRDS_REGION"
177	          remoteRef = {
178	            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
179	            property = "rds-region"
180	          }
181	        },
182	        {
183	          secretKey = "MRDS_INSTANCE_NAME"
184	          remoteRef = {
185	            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
186	            property = "rds-instance"
187	          }
188	        },
189	      ] : [], var.enable_nowrds_monitoring ? [
190	        {
191	          secretKey = "NOWRDS_HOST"
192	          remoteRef = {
193	            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
194	            property = "NOWRDS_HOST"
195	          }
196	        },
197	        {
198	          secretKey = "NOWRDS_PORT"
199	          remoteRef = {
200	            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
201	            property = "NOWRDS_PORT"
202	          }
203	        },
204	        {
205	          secretKey = "NOWRDS_USERNAME"
206	          remoteRef = {
207	            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
208	            property = "NOWRDS_USERNAME"
209	          }
210	        },
211	        {
212	          secretKey = "NOWRDS_PASSWORD"
213	          remoteRef = {
214	            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
215	            property = "NOWRDS_PASSWORD"
216	          }
217	        },
218	        {
219	          secretKey = "NOWRDS_INSTANCE_NAME"
220	          remoteRef = {
221	            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
222	            property = "NOWRDS_INSTANCE_NAME"
223	          }
224	        }
225	      ] : [])
226	    }
227	  }
228	  depends_on = [kubernetes_namespace.this]
229	}
230	# ---------------------------------------------------------------------------------------------------------------------
231	# DEPLOY A HELM CHART
232	# ---------------------------------------------------------------------------------------------------------------------
233	resource "helm_release" "this" {
234	  name      = var.name
235	  #namespace = kubernetes_namespace.this.metadata[0].name
236	  namespace = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
237	  wait      = false # Set to true if you want Terraform to wait for resources to be ready
238
239	  repository = "https://helm.datadoghq.com"
240	  chart      = "datadog"
241	  version    = var.chart_version
242	  values = [
243	    templatefile("${path.module}/values.yaml.tftpl", {
244	      cluster_name   = var.cluster_name
245	      env            = var.env == "staging" ? "nonprod" : var.env
246	      global_segment = var.global_segment
247	      image_tag      = var.image_tag
248	      watch_namespace          = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
249	      tags                     = concat(["global_segment:${var.global_segment}"], var.datadog_tags)
250	      agents_priority_class_name = var.agents_priority_class_name
251
252	      apm_enabled = var.apm_enabled
253
254	      container_exclude = var.container_exclude
255	      container_exclude_logs = var.container_exclude_logs
256
257	      enable_mrds_monitoring   = var.enable_mrds_monitoring
258	      enable_nowrds_monitoring = var.enable_nowrds_monitoring
259	      enable_now_obfusicator_options = var.enable_now_obfusicator_options
260	      now_obfusicator_options = var.now_obfusicator_options
261
262	      datadog_api_key_secret_name = var.datadog_api_key_secret_name
263	      cluster_agent_token_secret_name = var.cluster_agent_token_secret_name
264	      log_level = var.log_level
265	    })
266	  ]
267
268	  depends_on = [
269	    kubernetes_namespace.this,
270	    kubernetes_manifest.external-secret,
271	    kubernetes_manifest.external-secret-dd-api-key,
272	    kubernetes_manifest.external-secret-dd-api-token
273	  ]
274	}
275
276	module "netpol" {
277	  source = "git@gitlab.infrastructure.immuta.io:infrastructure/terraform/internal-modules/kubernetes/security/network-policy.git?ref=v1.1.0"
278
279	  name             = join("-", [var.name, "netpol"])
280	  namespace        = var.namespace
281	  network_policies = local.netpols
282
283	  tags = merge(var.tags, { "sourceModule" = "/internal-modules/kubernetes/monitoring/datadog" })
284
285	  depends_on = [
286	    kubernetes_namespace.this
287	  ]
288	}
289
290

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Deploys the Datadog Agent to a Kubernetes cluster using the official Helm chart.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

# ---------------------------------------------------------------------------------------------------------------------
# SET TERRAFORM REQUIREMENTS FOR RUNNING THIS MODULE
# ---------------------------------------------------------------------------------------------------------------------

terraform {
  required_version = ">= 1.3.0, < 2.0"

  required_providers {
    helm = {
      source  = "hashicorp/helm"
      version = "~> 2.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.0"
    }
  }
}

locals {
  apm_rules = file("${path.module}/processing-rules/${var.env}/apm.json")
  log_rules = file("${path.module}/processing-rules/${var.env}/rules.json")
  nowrds_instance = var.enable_nowrds_monitoring ? (<<-EON
    - dbm: true
      ssl: allow
      host: "%%env_NOWRDS_HOST%%"
      port: "%%env_NOWRDS_PORT%%"
      username: "%%env_NOWRDS_USERNAME%%"
      password: "%%env_NOWRDS_PASSWORD%%"
      collect_schemas:
        enabled: true
      database_autodiscovery:
        enabled: true
      tags:
        - "dbinstanceidentifier:%%env_NOWRDS_INSTANCE_NAME%%"
        - "env:%%env_DD_ENV%%"
  EON
  ) : null
}
# ---------------------------------------------------------------------------------------------------------------------
# CREATE A NAMESPACE
# ---------------------------------------------------------------------------------------------------------------------
resource "kubernetes_namespace" "this" {
  count = var.create_namespace ? 1 : 0
  metadata {
    name = var.namespace
  }
}

resource "kubernetes_config_map" "datadog_processing_rules" {
  metadata {
    name = "rules-config"
    namespace = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
  }
  data = {
    //"apm"  = local.apm_rules
    "apm" = file("${path.module}/processing-rules/${var.env}/apm.json")
    //"logs" = local.log_rules
    "logs" = file("${path.module}/processing-rules/${var.env}/rules.json")
  }
  depends_on = [kubernetes_namespace.this]
}

resource "kubernetes_manifest" "external-secret-dd-api-key" {
  manifest = {
    apiVersion = "external-secrets.io/v1"
    kind       = "ExternalSecret"
    metadata = {
      name      = "datadog-api-key"
      namespace = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
    }
    spec = {
      refreshInterval = "1h0m0s"
      secretStoreRef = {
        name = "vault"
        kind = "ClusterSecretStore"
      }
      target = {
        name           = "datadog-api-key"
        creationPolicy = "Owner"
      }
      data = [{
        secretKey = "api-key"
        remoteRef = {
          key      = "saas/infrastructure/datadog-api-key"
          property = "apiKey"
        }
      }]
    }
  }
}

resource "kubernetes_manifest" "external-secret-dd-api-token" {
  manifest = {
    apiVersion = "external-secrets.io/v1"
    kind       = "ExternalSecret"
    metadata = {
      name      = "datadog-clusteragent-token"
      namespace = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
    }
    spec = {
      refreshInterval = "1h0m0s"
      secretStoreRef = {
        name = "vault"
        kind = "ClusterSecretStore"
      }
      target = {
        name           = "datadog-clusteragent-token"
        creationPolicy = "Owner"
      }
      data = [{
        secretKey = "token"
        remoteRef = {
          key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
          property = "token"
        }
      }]
    }
  }
}

resource "kubernetes_manifest" "external-secret" {
  count = (var.enable_mrds_monitoring || var.enable_nowrds_monitoring) ? 1 : 0

  manifest = {
    apiVersion = "external-secrets.io/v1"
    kind       = "ExternalSecret"
    metadata = {
      name      = "datadog-env"
      namespace = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
    }
    spec = {
      refreshInterval = "1h0m0s"
      secretStoreRef = {
        name = "vault"
        kind = "ClusterSecretStore"
      }
      target = {
        name           = "datadog-env"
        creationPolicy = "Owner"
      }
      data = concat(var.enable_mrds_monitoring ? [
        {
          secretKey = "MRDS_HOST"
          remoteRef = {
            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
            property = "rds-host"
          }
        },
        {
          secretKey = "MRDS_PORT"
          remoteRef = {
            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
            property = "rds-port"
          }
        },
        {
          secretKey = "MRDS_USERNAME"
          remoteRef = {
            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
            property = "rds-username"
          }
        },
        {
          secretKey = "MRDS_PASSWORD"
          remoteRef = {
            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
            property = "rds-password"
          }
        },
        {
          secretKey = "MRDS_REGION"
          remoteRef = {
            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
            property = "rds-region"
          }
        },
        {
          secretKey = "MRDS_INSTANCE_NAME"
          remoteRef = {
            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
            property = "rds-instance"
          }
        },
      ] : [], var.enable_nowrds_monitoring ? [
        {
          secretKey = "NOWRDS_HOST"
          remoteRef = {
            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
            property = "NOWRDS_HOST"
          }
        },
        {
          secretKey = "NOWRDS_PORT"
          remoteRef = {
            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
            property = "NOWRDS_PORT"
          }
        },
        {
          secretKey = "NOWRDS_USERNAME"
          remoteRef = {
            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
            property = "NOWRDS_USERNAME"
          }
        },
        {
          secretKey = "NOWRDS_PASSWORD"
          remoteRef = {
            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
            property = "NOWRDS_PASSWORD"
          }
        },
        {
          secretKey = "NOWRDS_INSTANCE_NAME"
          remoteRef = {
            key      = "saas/infrastructure/datadog-helper/${var.env}/${var.global_segment}"
            property = "NOWRDS_INSTANCE_NAME"
          }
        }
      ] : [])
    }
  }
  depends_on = [kubernetes_namespace.this]
}
# ---------------------------------------------------------------------------------------------------------------------
# DEPLOY A HELM CHART
# ---------------------------------------------------------------------------------------------------------------------
resource "helm_release" "this" {
  name      = var.name
  #namespace = kubernetes_namespace.this.metadata[0].name
  namespace = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
  wait      = false # Set to true if you want Terraform to wait for resources to be ready

  repository = "https://helm.datadoghq.com"
  chart      = "datadog"
  version    = var.chart_version
  values = [
    templatefile("${path.module}/values.yaml.tftpl", {
      cluster_name   = var.cluster_name
      env            = var.env == "staging" ? "nonprod" : var.env
      global_segment = var.global_segment
      image_tag      = var.image_tag
      watch_namespace          = var.create_namespace ? kubernetes_namespace.this[0].metadata[0].name : var.namespace
      tags                     = concat(["global_segment:${var.global_segment}"], var.datadog_tags)
      agents_priority_class_name = var.agents_priority_class_name

      apm_enabled = var.apm_enabled

      container_exclude = var.container_exclude
      container_exclude_logs = var.container_exclude_logs

      enable_mrds_monitoring   = var.enable_mrds_monitoring
      enable_nowrds_monitoring = var.enable_nowrds_monitoring
      enable_now_obfusicator_options = var.enable_now_obfusicator_options
      now_obfusicator_options = var.now_obfusicator_options

      datadog_api_key_secret_name = var.datadog_api_key_secret_name
      cluster_agent_token_secret_name = var.cluster_agent_token_secret_name
      log_level = var.log_level
    })
  ]

  depends_on = [
    kubernetes_namespace.this,
    kubernetes_manifest.external-secret,
    kubernetes_manifest.external-secret-dd-api-key,
    kubernetes_manifest.external-secret-dd-api-token
  ]
}

module "netpol" {
  source = "git@gitlab.infrastructure.immuta.io:infrastructure/terraform/internal-modules/kubernetes/security/network-policy.git?ref=v1.1.0"

  name             = join("-", [var.name, "netpol"])
  namespace        = var.namespace
  network_policies = local.netpols

  tags = merge(var.tags, { "sourceModule" = "/internal-modules/kubernetes/monitoring/datadog" })

  depends_on = [
    kubernetes_namespace.this
  ]
}