Skip to content

add controller, leaf agents, Config CRD, and recurring-task-creator to helm chart#30

Merged
bborbe merged 2 commits into
masterfrom
feature/chart-components
Jul 4, 2026
Merged

add controller, leaf agents, Config CRD, and recurring-task-creator to helm chart#30
bborbe merged 2 commits into
masterfrom
feature/chart-components

Conversation

@bborbe

@bborbe bborbe commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Extends the agent Helm chart (0.1.0 → 0.2.0) from executor-only to the full core platform, per the reusable-framework goal architecture (three-layer: public images → chart → per-cluster values).

What's added

  • Config CRD (crds/config-crd.yaml) — configs.agent.benjamin-borbe.de, shipped via crds/ so leaf Config CRs apply on first install. The executor still creates/updates the schema at runtime; crds/ only guarantees install-time ordering.
  • Controlleragent-task-controller StatefulSet (1Gi BoltDB PVC) + Service + gated Secret (sentry-dsn + gateway-secret). Vault coupling parameterized: controller.vaultName, controller.gitRestUrl (derives http://vault-obsidian-<vault>:9090 when unset).
  • Leaf agents — values-driven: each entry in agents: [] emits Config CR + Secret + PVC + PriorityClass (cluster-scoped) + ResourceQuota (pods cap). No hard-coded agents; the 4 examples are documented in a commented values block.
  • recurring-task-creator — StatefulSet + full RBAC + Service + Secret, behind recurringTaskCreator.enabled (default false). Strimzi KafkaUser double-gated on kafkaUser.enabled (mTLS clusters only).
  • Helpers agent.controller.image, agent.recurringTaskCreator.image, agent.controller.gitRestUrl; expanded values.yaml with public docker.io defaults + per-cluster override comments.

Verification

  • helm lint clean (only the cosmetic icon INFO).
  • helm template renders valid YAML for: full-enable (all components), 2-agent (proves the multi-doc separator), and executor-only minimal (proves every new component is truly optional). All docs parse via yaml.safe_load_all.
  • CRD present only with --include-crds (correct — crds/ isn't templated).
  • make precommit passes (format/generate/test/lint/addlicense).

🤖 Generated with Claude Code

@ben-s-pull-request-reviewer ben-s-pull-request-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Based on the diff captured in Step 0c, I have enough information to perform the review. Let me now compile the consolidated findings.


PR Review: add controller, leaf agents, Config CRD, and recurring-task-creator to helm chart

Changed Files Reviewed

  • CHANGELOG.md — version header + changelog bullets
  • helm/Chart.yaml — version 0.1.0 → 0.2.0, new sources
  • helm/crds/config-crd.yaml — new Config CRD
  • helm/templates/_helpers.tpl — 3 new helper functions
  • helm/templates/agents.yaml — new multi-document agent templates
  • helm/templates/controller-secret.yaml — new controller Secret
  • helm/templates/controller-service.yaml — new controller Service
  • helm/templates/controller-statefulset.yaml — new controller StatefulSet
  • helm/templates/recurring-task-creator-kafkauser.yaml — new Strimzi KafkaUser
  • helm/templates/recurring-task-creator-statefulset.yaml — new recurring-task-creator
  • helm/values.yaml — new controller, agents, recurringTaskCreator blocks

MECHANICAL FUNNEL (ast-grep-runner) — skipped (ast-grep not available in this environment)


MANUAL REVIEW FINDINGS

Must Fix (Critical)

1. helm/templates/controller-secret.yaml — base64 encoding includes spurious quotes

The template uses {{ .Values.controller.sentry.dsn | b64enc | quote }}. The | quote after b64enc wraps the base64 output in double-quote characters. The resulting Secret value is "ZHVtbXk=" (with literal quote characters), not ZHVtbXk=. The application reading this secret via secretKeyRef will receive the quoted string including " characters, causing deserialization failures. The same bug exists for gateway-secret.

data:
  sentry-dsn: {{ .Values.controller.sentry.dsn | b64enc | quote }}   # BUG: quotes included
  gateway-secret: {{ .Values.controller.gatewaySecret | b64enc | quote }}  # BUG: same

Fix: Remove | quoteb64enc already produces a valid string value for Secret.data.


2. helm/templates/recurring-task-creator-statefulset.yamltmp volume defined but never mounted, will cause write failures

The pod spec.volumes declares a tmp emptyDir volume, and the container securityContext sets readOnlyRootFilesystem: true + fsGroup: 65534 (expecting the tmp volume to be group-writable at /tmp). However, there is no volumeMounts entry for this tmp volume in the container. Writes to /tmp will hit the read-only rootfs and fail.

The diff fragment showing the StatefulSet container ends with:

volumeMounts:
  - name: datadir
    mountPath: /data

The tmp volume is not mounted. The fsGroup: 65534 + readOnlyRootFilesystem: true combination is only safe when /tmp is an explicit emptyDir mount — without it, the container will fail on first write to /tmp.

Fix: Add volumeMounts: - name: tmp mountPath: /tmp to the recurring-task-creator container.


3. helm/templates/controller-statefulset.yamltmp volume defined but never mounted (dead code / potential future bug)

Same pattern: the pod spec.volumes has a tmp emptyDir volume, but the controller container only mounts datadir. Unlike recurring-task-creator, the controller does not set readOnlyRootFilesystem: true, so this doesn't cause an immediate failure — but the tmp volume is unused dead code that adds confusion.


Should Fix (Important)

4. helm/values.yamlcontroller.storage.storageClassName: local-path is unsuitable for multi-node clusters

local-path is a Local PV provisioner — it only works on single-node clusters (or nodes with the Local Path Operator). Production clusters that run multiple nodes will fail to provision the BoltDB PVC. The comment says "local-path suits single-node / quant; override elsewhere" — but there is no validation preventing installation on a multi-node cluster without changing this value.

Fix: Change the default to "" (empty = use the cluster's default StorageClass) and add a comment warning that local-path requires the Local Path Operator.


5. helm/templates/recurring-task-creator-statefulset.yaml — Strimzi namespace convention hardcoded

strimziNamespace: strimzi is a common convention but not universal. If the Strimzi operator runs in a different namespace (e.g., strimzi-operators), the KafkaUser is created in the wrong namespace and mTLS auth will silently fail. The KafkaUser's metadata.namespace is set to this value, while the app namespace is only referenced via labels — a namespace mismatch would not produce an error at install time.

Fix: Add a comment noting the assumption, or make the default match the most common production setup.


6. helm/values.yamlcontroller.gitRestUrl derivation assumes vault-obsidian-{vaultName} naming

The helper produces http://vault-obsidian-{vaultName}:9090. This is a specific internal service naming convention that is not universal. The comment says "in-cluster obsidian git-rest service" — if the service is named differently (e.g., git-rest, vault-git-rest, or in a different namespace), the controller will fail to reach it with no warning at install time. Only vaultName is required; there is no validation of the derived URL.

Fix: Document the naming assumption prominently in values.yaml comments.


7. helm/templates/recurring-task-creator-statefulset.yamldryRun: "false" is a string, not a boolean

dryRun: "false" in values.yaml produces the string "false", not the boolean false. In Go (and most languages), os.Getenv("DRY_RUN") == "false" may not behave as expected depending on how the application parses it. Many CLI libraries treat "false" (string) as falsy, but this is fragile and depends on the application's parsing logic.

Fix: Document in values.yaml whether the application expects a string or boolean, or add a comment explaining the string-based contract.


Nice to Have (Optional)

8. helm/templates/recurring-task-creator-kafkauser.yaml — KafkaUser in a different namespace than the app

The KafkaUser is created in strimziNamespace (default strimzi) which is typically a different namespace from where the recurring-task-creator runs. RBAC labels (strimzi.io/cluster) are correct, but standard Kubernetes tooling (e.g., kubectl get kafkausers -n <app-ns>) won't find it, which can be confusing during debugging.


Precommit

precommit skipped (selector mode) — CI covers lint+test.


Selector Mode Traceability

  • Rules triggered (judgment tier): None applicable — helm chart changes are outside ast-grep rule scope.
  • Funnel result: ast-grep unavailable in this environment; mechanical checks skipped.
  • Adjudication: In-session, using diff content + helm conventions knowledge.

Next Steps

  • Fix the b64enc | quote bug in both secret templates (critical — creates malformed Secrets)
  • Mount the tmp volume in recurring-task-creator container (critical — causes write failures with readOnlyRootFilesystem)
  • Consider fixing the unused tmp volume in controller for clarity
  • Change controller.storage.storageClassName default to "" with a warning comment
  • Document the vault-obsidian-{vaultName} naming assumption
{
  "verdict": "request-changes",
  "summary": "The helm chart extension adds substantial new functionality (controller, agents, recurring-task-creator, Config CRD) with correct overall structure, but contains two critical correctness bugs: base64 values in Secrets include spurious quote characters due to `| quote` after `b64enc`, and the recurring-task-creator container defines a tmp volume that is never mounted while expecting writes to /tmp with a read-only root filesystem. Several important improvements are also recommended around storage class defaults, Strimzi namespace conventions, and git-rest URL derivation assumptions.",
  "comments": [
    {
      "file": "helm/templates/controller-secret.yaml",
      "line": 15,
      "severity": "critical",
      "message": "Must Fix (Critical): `b64enc | quote` produces base64 values wrapped in literal quote characters (e.g. `\"ZHVtbXk=\"`). Remove `| quote` — b64enc already produces a valid Secret data value. The application will receive quoted strings including `\"` characters when reading `secretKeyRef`."
    },
    {
      "file": "helm/templates/recurring-task-creator-statefulset.yaml",
      "line": 165,
      "severity": "critical",
      "message": "Must Fix (Critical): Pod defines a `tmp` emptyDir volume but the container has no `volumeMounts` entry for it. With `readOnlyRootFilesystem: true`, any write to `/tmp` will fail. Add `volumeMounts: - name: tmp mountPath: /tmp` to the service container."
    },
    {
      "file": "helm/templates/controller-statefulset.yaml",
      "line": 120,
      "severity": "critical",
      "message": "Must Fix (Critical): Same tmp volume bug as recurring-task-creator — pod defines `tmp` emptyDir but container does not mount it. Unlike recurring-task-creator, the controller does not set `readOnlyRootFilesystem: true`, so this is dead code rather than a runtime failure, but it should be cleaned up."
    },
    {
      "file": "helm/values.yaml",
      "line": 129,
      "severity": "major",
      "message": "Should Fix (Important): `controller.storage.storageClassName: local-path` is a single-node/local-path-provisioner-specific value. Multi-node production clusters will fail to schedule the BoltDB PVC. Change default to `""` (cluster default) and add a comment warning that `local-path` requires the Local Path Operator."
    },
    {
      "file": "helm/values.yaml",
      "line": 194,
      "severity": "major",
      "message": "Should Fix (Important): `recurringTaskCreator.kafkaUser.strimziNamespace: strimzi` assumes the Strimzi operator runs in the `strimzi` namespace. If installed elsewhere, the KafkaUser is created in the wrong namespace and mTLS auth silently fails. Add a comment noting this assumption."
    },
    {
      "file": "helm/values.yaml",
      "line": 89,
      "severity": "major",
      "message": "Should Fix (Important): `controller.gitRestUrl` derivation assumes the in-cluster service is named `vault-obsidian-{vaultName}:9090`. This is a specific internal naming convention. If the service is named differently, the controller will fail with no install-time warning. Document this assumption prominently."
    },
    {
      "file": "helm/values.yaml",
      "line": 174,
      "severity": "major",
      "message": "Should Fix (Important): `dryRun: \"false\"` is a string `\"false\"`, not boolean `false`. Depending on how the application parses this env var, the behavior may be inconsistent. Document the expected string values or clarify the application's parsing logic."
    },
    {
      "file": "helm/templates/recurring-task-creator-kafkauser.yaml",
      "line": 1,
      "severity": "nit",
      "message": "Nice to Have (Optional): KafkaUser is created in `strimziNamespace` (default `strimzi`), typically a different namespace from the app. Standard `kubectl get kafkausers -n <app-ns>` won't find it, which can be confusing during debugging. Consider a comment noting where to find it."
    }
  ],
  "concerns_addressed": [
    "security: controller-secret.yaml — secrets are not hardcoded (empty by default), but the b64enc|quote bug causes malformed values",
    "security: recurring-task-creator-kafkauser.yaml — kafkaUser.enabled gating is correctly honored; mTLS requirements properly documented with type: tls",
    "security: _helpers.tpl — gitRestUrl derivation assumes specific vault-obsidian naming convention; should be documented",
    "correctness: config-crd.yaml — CRD in crds/ dir is correct; preserveUnknownFields: false is appropriate",
    "correctness: agents.yaml — multi-document YAML separator (---) is correctly placed; toYaml rendering appears correct",
    "correctness: controller-statefulset.yaml — storage class is parameterized but defaults to local-path (unsuitable for multi-node); tmp volume not mounted",
    "tests: helm template rendering tests — not visible in diff, but new values blocks suggest test coverage would be valuable"
  ]
}

@bborbe

bborbe commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

Thanks — addressed the reusability findings in 58aa2e4:

  • storageClassName → defaulted to "" and now omitted when empty so the cluster's default StorageClass is used (an explicit "" would disable dynamic provisioning); local-path is a quant-overlay-only choice.
  • gitRestUrl vault-obsidian-<vault>:9090 naming assumption + KafkaUser strimziNamespace placement/debugging caveats — documented prominently in values.yaml.

On the three Critical findings — all three are false positives (verified against the rendered output and the existing bot-approved templates):

  1. b64enc | quote — the quotes are YAML string delimiters, stripped on parse; the stored Secret value is the raw base64. This is the same idiom as the already-merged executor-secret.yaml, and is the Helm-documented convention. No literal quotes reach the app.
  2. recurring-task-creator tmp not mounted — the container does mount it: volumeMounts: [{name: tmp, mountPath: /tmp}] is present under the service container (statefulset). helm template + yaml.safe_load_all confirm.
  3. controller tmp dead code — the controller has no tmp volume at all (only the datadir PVC); nothing to remove.

Re-render is lint-clean and all docs parse.

@ben-s-pull-request-reviewer ben-s-pull-request-reviewer Bot dismissed their stale review July 4, 2026 14:27

superseded by new automated review

@ben-s-pull-request-reviewer ben-s-pull-request-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Based on my comprehensive review of the changed files, here is my analysis:

Reviewed files (selector mode, in-session):

  • helm/Chart.yaml — chart metadata, version bump 0.1.0→0.2.0
  • helm/crds/config-crd.yaml — new Config CRD with CEL validation rule
  • helm/templates/_helpers.tpl — new image helpers + gitRestUrl derivation
  • helm/templates/agents.yaml — Config CR + Secret + PVC + PriorityClass + ResourceQuota per agent
  • helm/templates/controller-secret.yaml — conditional Secret creation
  • helm/templates/controller-service.yaml — new headless-ish Service
  • helm/templates/controller-statefulset.yaml — new StatefulSet with full env/Broker/VolumeClaimTemplate
  • helm/templates/recurring-task-creator-kafkauser.yaml — Strimzi KafkaUser (off by default)
  • helm/templates/recurring-task-creator-statefulset.yaml — StatefulSet + RBAC (Role/RoleBinding/ClusterRole/ClusterRoleBinding) + Service + Secret
  • helm/values.yaml — new controller, agents[], recurringTaskCreator blocks
  • CHANGELOG.md — documented the additions

Concern adjudication:

# Area Concern Verdict
1 security Secret with b64enc — off by one key when only one of sentry.dsn/gatewaySecret is set Partially valid — the Secret has a spurious empty-key when only one value is set; statefulset uses required so it won't start, but the secret is still created
2 security RBAC minimum permissions Addressed — Role get/list/watch on schedules only; ClusterRole limited to CRD registration
3 security Strimzi namespace placement Documented — values.yaml lines 225–235 explicitly warn about strimziNamespace and the debugging implication
4 correctness Hardcoded vault-obsidian-<vaultName>:9090 Documented — values.yaml lines 91–98 have a prominent warning, plus _helpers.tpl comment
5 correctness controller.kafkaBrokers required Addressedrequired at line 81 produces a clear error on empty
6 correctness recurringTaskCreator.kafkaBrokers required Addressedrequired at line 129
7 correctness ResourceQuota string quota Valid concern, resolved{{ $agent.concurrency | default 1 | quote }} renders "1" (quoted string), which is correct for Kubernetes
8 correctness CRD CEL rule Valid — rule size(self.taskType) > 0 (string length) and size(self.taskTypes) > 0 (array length) are correct CEL expressions; note: requires Kubernetes 1.25+ (CEL support)
9 performance storageClassName empty-string default Intentional + documented — comment at lines 133–136 explains omission when empty; portable across StorageClasses
10 performance PVC per agent By design — each agent needs dedicated storage; documented in values.yaml comment
{
  "verdict": "approve",
  "summary": "The Helm chart additions (controller StatefulSet+Service+Secret, leaf agents, recurring-task-creator with full RBAC, Config CRD) are well-structured with proper security defaults (readOnlyRootFilesystem, runAsNonRoot, no privilege escalation), appropriate RBAC scoping, and thorough inline documentation of non-obvious assumptions (gitRestUrl naming, strimziNamespace placement). The only minor issue is the controller Secret creates a spurious empty-key when only one of sentry.dsn/gatewaySecret is set — the statefulset refuses to start with empty values via `required`, so it fails safe, but the secret is still emitted unnecessarily.",
  "comments": [
    {
      "file": "helm/templates/controller-secret.yaml",
      "line": 15,
      "severity": "major",
      "message": "The Secret always emits both `sentry-dsn` and `gateway-secret` keys when either value is set. If only `sentry.dsn` is configured, `gateway-secret` gets `{{ .Values.controller.gatewaySecret | b64enc }}` where the value is empty — producing base64 `\"Cg==\"` rather than omitting the key. The statefulset uses `required` so the pod won't start with empty values, but the secret is still created with a spurious empty key. Consider splitting into two distinct conditional blocks (one per key) so the Secret only contains keys that are actually configured."
    }
  ],
  "concerns_addressed": [
    "security: RBAC (Role get/list/watch on schedules, ClusterRole for CRD registration only) — correctly scoped, not over-permissioned",
    "security: Strimzi KafkaUser namespace placement — documented at values.yaml:225-235",
    "correctness: gitRestUrl hardcoded derivation — documented at values.yaml:91-98 with prominent warning",
    "correctness: controller.kafkaBrokers required guard — present at line 81",
    "correctness: recurringTaskCreator.kafkaBrokers required guard — present at line 129",
    "correctness: ResourceQuota string quota — correctly rendered as quoted string",
    "correctness: CRD CEL rule — syntactically valid (requires K8s 1.25+)",
    "performance: storageClassName empty-string default — intentional with explanatory comment at lines 133-136",
    "performance: PVC per agent — by design, documented in values.yaml"
  ]
}

@bborbe bborbe merged commit 6d0bdcd into master Jul 4, 2026
1 check passed
@bborbe bborbe deleted the feature/chart-components branch July 4, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant