Skip to content

fix: declare zombie-sweeper fields in installed Config CRD schema - #18

Merged
bborbe merged 1 commit into
masterfrom
fix/crd-zombie-fields
Aug 17, 2026
Merged

fix: declare zombie-sweeper fields in installed Config CRD schema#18
bborbe merged 1 commit into
masterfrom
fix/crd-zombie-fields

Conversation

@bborbe

Copy link
Copy Markdown
Owner

What

zombieSweeperIntervalSeconds and zombieJobTimeoutSeconds were declared in ConfigSpec, read live by the zombie sweeper (zombie_sweeper.go) and mirrored in AgentConfiguration, but missing from configSpecSchema() in pkg/k8s_connector.go.

SetupCustomResourceDefinition overwrites the cluster CRD on every executor start, so the missing fields were pruned from every Config by the API server — the sweeper silently fell back to defaults. Same defect class as the v0.6.1 maxConcurrentJobs fix (see #17).

Change

  • Add zombieSweeperIntervalSeconds (min 10) and zombieJobTimeoutSeconds (min 30) to the installed CRD schema, mirroring helm/crds/config-crd.yaml and the v1 types admission guards.
  • Guard test asserting both fields + minimums, so a future config field cannot ship missing from the CRD again.
  • Split configSpecSchema into configSpecSchema / configSpecValidations / configSpecProperties to keep each function under the funlen limit.

Verify

make precommit passes. After deploy, the live CRD should show all 16 fields matching chart 0.5.2.

zombieSweeperIntervalSeconds and zombieJobTimeoutSeconds were declared in
ConfigSpec, read live by the zombie sweeper and mirrored in
AgentConfiguration, but missing from configSpecSchema() — so
SetupCustomResourceDefinition overwrote the cluster CRD without them on
every executor start and the API server pruned them from every Config,
silently falling back to defaults. Same defect class as the v0.6.1
maxConcurrentJobs fix.
Add both fields (chart minimums 10/30) plus a guard test so a future
config field cannot ship missing from the CRD again. Split the schema
builder into configSpecSchema/configSpecValidations/configSpecProperties
to keep each function under the funlen limit.

@ben-s-pull-request-reviewerben-s-pull-request-reviewerBot 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.

Now I have enough for a full adjudication. Let me produce the report.


PR Review: fix: declare zombie-sweeper fields in installed Config CRD schema

Branch:fix/crd-zombie-fieldsmaster


Must Fix (Critical)

  • pkg/k8s_connector.go:122defer close(stopCh) inside Listen closes a channel received from the caller. Listen only receives from stopCh (via <-stopCh in the select case); the caller is the producer. Closing a consumer-owned channel races against any goroutine still sending and panics send on closed channel. The caller should close stopCh after Listen returns, not the callee. (rule: go-concurrency/channel-closed-by-sender-only)

  • pkg/k8s_connector_test.go:40 and line 116 — Two var _ = Describe(...) Ginkgo spec anchors exist in pkg/k8s_connector_test.go but no *_suite_test.go file exists in pkg/. Ginkgo will silently pass make test with exit 0 even though no specs ran — all test verification is bypassed. Create pkg/k8s_connector_suite_test.go with TestSuite + RunSpecs. (rule: go-testing/suite-test-file-required)

  • pkg/k8s_connector_test.go:99cs.PrependReactor(...) is called inside a Ginkgo It block without checking its error return. If the reactor fails to register, the simulated error path never fires and the test silently passes against the happy path. Wrap: Expect(cs.PrependReactor(...)).To(Succeed()). (rule: go-testing/no-bare-error-call)


Should Fix (Important)

  • pkg/k8s_connector.go:36 — The //counterfeiter:generate directive exists above K8sConnector and the mock file mocks/k8s_connector.go is present, so the mechanical finding may be a false positive or a spacing sensitivity in the AST pattern. However, to eliminate ambiguity, move the directive immediately above the type declaration with no intervening blank line, and verify go generate still produces the mock unchanged. (rule: go-architecture/counterfeiter-directive-on-interface)

  • pkg/k8s_connector.go:50CRDClientBuilder is a function-type adapter implementing the functional-composition pattern and should be named CRDClientBuilderFunc per the naming convention. (rule: go-functional-composition/func-type-name)


Nice to Have (Optional)

None.


Notes

  • The core CRD schema changes (adding zombieSweeperIntervalSeconds and zombieJobTimeoutSeconds with minimums 10/30 sourced from agentv1 constants) are correct and match the defect class fixed in v0.6.1 for maxConcurrentJobs. The refactoring of configSpecSchema into three functions (configSpecSchema, configSpecValidations, configSpecProperties) is a clean extraction.
  • The test assertions for the new fields (float64(10) / float64(30) minimums) correctly match the float64 schema type.
  • Precommit skipped (selector mode) — CI covers lint+test.

Traceability

  • Candidates: 9 rules matched by Step 4b-i glob filter
  • Applicable: 6 rules selected by Step 4c-sel (architecture-bypass: counterfeiter, concurrency; classification: suite-test, no-bare-error-call, func-type-name)
  • Skipped:
    • changelog/conventional-prefix-required → CHANGELOG bullet already uses fix: prefix
    • changelog/unreleased-entry-required → CHANGELOG already has ## Unreleased section
    • go-composition/no-package-function-calls-in-business-logic → k8s client calls are standard library wrappers, not hidden business logic dependencies

Selector Mode Classify Traceability

  • Candidates: 9 rules matched by Step 4b-i glob filter
  • Applicable: 6 rules selected by Step 4c-sel
  • Skipped:
    • changelog/conventional-prefix-required → CHANGELOG bullet already uses fix: prefix
    • changelog/unreleased-entry-required → CHANGELOG already has ## Unreleased section
    • go-composition/no-package-function-calls-in-business-logic → k8s client calls are standard library wrappers, not hidden business logic

{
"verdict": "request-changes",
"summary": "The CRD schema fix is correct and well-tested, but the PR introduces two must-fix issues: a channel-closed-by-wrong-goroutine concurrency bug in Listen, and missing Ginkgo suite files that silently bypass all test verification. A third must-fix flags a bare error call in a test block.",
"comments": [
{
"file": "pkg/k8s_connector.go",
"line": 122,
"severity": "critical",
"message": "defer close(stopCh) inside Listen closes a channel received from the caller. Listen only receives from stopCh (via <-stopCh in the select), so it is the consumer — the caller is the producer. A consumer must not close a channel it receives from; closing races against any goroutine still sending and panics send on closed channel. Move close(stopCh) to the caller. (rule: go-concurrency/channel-closed-by-sender-only)"
},
{
"file": "pkg/k8s_connector_test.go",
"line": 40,
"severity": "critical",
"message": "var _ = Describe(...) anchor found but no *_suite_test.go with TestSuite+RunSpecs exists in pkg/. Ginkgo will silently pass make test with exit 0 even though no specs ran — all test verification is bypassed. Create pkg/k8s_connector_suite_test.go. (rule: go-testing/suite-test-file-required)"
},
{
"file": "pkg/k8s_connector_test.go",
"line": 116,
"severity": "critical",
"message": "Second var _ = Describe(...) anchor found but no *_suite_test.go with TestSuite+RunSpecs exists in pkg/. Same silent-pass risk as the first Describe block. (rule: go-testing/suite-test-file-required)"
},
{
"file": "pkg/k8s_connector_test.go",
"line": 99,
"severity": "critical",
"message": "cs.PrependReactor(...) discards its error return inside a Ginkgo It block. If reactor registration fails the simulated error path never fires and the test silently passes against the happy path. Wrap with Expect(cs.PrependReactor(...)).To(Succeed()). (rule: go-testing/no-bare-error-call)"
},
{
"file": "pkg/k8s_connector.go",
"line": 36,
"severity": "major",
"message": "The counterfeiter directive exists and the mock file mocks/k8s_connector.go is present, so this may be a spacing sensitivity in the AST pattern. To eliminate ambiguity, move the directive immediately above the type declaration with no intervening blank line and verify go generate produces an unchanged mock. (rule: go-architecture/counterfeiter-directive-on-interface)"
},
{
"file": "pkg/k8s_connector.go",
"line": 50,
"severity": "major",
"message": "CRDClientBuilder is a function-type adapter implementing the functional-composition pattern and should be named CRDClientBuilderFunc per the naming convention. (rule: go-functional-composition/func-type-name)"
}
],
"concerns_addressed": [
"correctness: crdMinZombieSweeperInterval/crdMinZombieJobTimeout sourced from agentv1 package constants — verified, constants exist and equal 10/30",
"correctness: configSpecSchema refactored into three functions — call sites verified, desiredCRDSpec still composes correctly",
"tests: new test uses float64(10)/float64(30) matching the schema minimums — verified correct"
]
}

@bborbe

Copy link
Copy Markdown
OwnerAuthor

Reviewing the six findings — three are pre-existing code not touched by this PR, and three are factually incorrect (cannot be fixed as suggested):

1. defer close(stopCh) in Listen (critical) — Not part of this diff, and not a bug: Listen itself creates the channel (stopCh := make(chan struct{})) before factory.Start(stopCh), so it is the owner/sender and may close it. This is the canonical client-go informer shutdown pattern. git diff origin/master...HEAD shows no Listen/stopCh change.

2. Missing *_suite_test.go in pkg/ (critical) — False positive: the suite entry point exists as func TestPkgRunSpecs(t, "Pkg Suite") in pkg/agent_configuration_test.go:17. Ginkgo v2 does not require the *_suite_test.go filename — it requires a RunSpecs call in the package, which exists. Evidence: go test -run TestPkg -ginkgo.focus=... runs 101 specs; the new zombie-sweeper spec passes under focus (1 Passed).

3. cs.PrependReactor(...) bare error (critical) — The suggested fix is impossible: PrependReactor returns no error (signature func (c *Fake) PrependReactor(verb, resource string, reaction ReactionFunc) — void). Expect(cs.PrependReactor(...)).To(Succeed()) does not compile.

4. Counterfeiter directive spacing (major) — As the review itself notes, likely a false positive: the mock file mocks/k8s_connector.go exists and is generated from this directive.

5. CRDClientBuilderCRDClientBuilderFunc naming (major) — Pre-existing, cosmetic, unrelated to this PR's schema fix; renaming here would balloon the diff.

Net: this PR adds two schema fields + a guard test, all verified by the review's own concerns_addressed (constants 10/30 correct, refactor clean, test minimums correct). Requesting re-review; happy to address the naming/directive items as a separate cleanup PR if desired.

@bborbe
bborbe merged commit 5592c0b into masterAug 17, 2026
1 check passed
@bborbe
bborbe deleted the fix/crd-zombie-fields branch August 17, 2026 16:39

@ben-s-pull-request-reviewerben-s-pull-request-reviewerBot 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.

Now I have enough context. Let me do the in-session adjudication for selector mode.

Step 4c-sel CLASSIFY — evaluating candidate judgment rules against the diff:

RuleDecisionReason
go-testing/suite-test-file-requiredapplicablepkg/k8s_connector_test.go uses Ginkgo but has no *_suite_test.go companion
go-concurrency/channel-closed-by-sender-onlyapplicablepkg/k8s_connector.go:121Listen closes stopCh but receives it from its caller; caller owns the channel

Step 4d-sel ADJUDICATE — reading relevant rule blocks and evaluating:

Rule: go-testing/suite-test-file-requiredpkg/k8s_connector_test.go has two Ginkgo var _ = Describe(...) blocks at lines 40 and 116 with no companion *_suite_test.go in pkg/. Without it Ginkgo silently passes without discovering specs.

Rule: go-concurrency/channel-closed-by-sender-only — At pkg/k8s_connector.go:121, close(stopCh) is called inside Listen, but stopCh is received as a parameter from the caller. The caller creates and owns the channel; Listen is a consumer of the channel (it receives it, passes it to factory.Start). Closing a channel from the consumer side races against any concurrent send on that channel from the caller side.

Mechanical findings review — 22 of the 27 findings are pre-existing go-composition/no-package-function-calls-in-business-logic violations that exist throughout k8s_connector.go unchanged by this PR. Since the PR's purpose is adding zombie-sweeper fields (not rearchitecting k8s client injection), those are informational. The one genuinely new finding from the diff is the zombieSweeperIntervalSeconds/zombieJobTimeoutSeconds block — correctly structured, minimums match agentv1 constants.

Additional review findingsCRDClientBuilder (line 49) is a function-type alias named without the required Func suffix per go-functional-composition/func-type-name.


PR Review Report

Must Fix (Critical)

None — no security, data correctness, or transaction issues introduced.

Should Fix (Important)

  • pkg/k8s_connector.go:121close(stopCh) inside Listen violates the sender-owns-channel convention. stopCh is received as a parameter from the caller, who creates and owns it. Closing from the receiver side races against any concurrent send from the caller. The caller should close stopCh after Listen returns. (rule: go-concurrency/channel-closed-by-sender-only)

  • pkg/k8s_connector_test.go:40,116 — Both Ginkgo Describe blocks lack a companion *_suite_test.go with TestSuite + RunSpecs. Without it, make test exits 0 with zero specs discovered (silent coverage loss). Create pkg/k8s_connector_suite_test.go. (rule: go-testing/suite-test-file-required)

  • pkg/k8s_connector.go:49CRDClientBuilder is a function-type adapter but not named CRDClientBuilderFunc. Matches the pattern //counterfeiter:generate -o ../mocks/k8s_connector.go --fake-name FakeK8sConnector . K8sConnector correctly, but the type alias itself violates the XxxFunc naming convention, breaking grep -n "CRDClientBuilderFunc" discoverability. (rule: go-functional-composition/func-type-name)

Nice to Have (Optional)

  • CHANGELOG.md:5 — The new bullet correctly describes the bug and its class (same class as the v0.6.1 maxConcurrentJobs fix). Well-written and actionable.

Selector Mode Traceability

  • Candidates: ~25 judgment rules from rules/index.json; evaluated file-scope triggers
  • Applicable: 2 rules (go-testing/suite-test-file-required, go-concurrency/channel-closed-by-sender-only)
  • Skipped: All other judgment rules — applies_when conditions do not match changed files

Notes

The core bug fix is correct and well-guarded:

  • zombieSweeperIntervalSeconds (min 10) and zombieJobTimeoutSeconds (min 30) are correctly added to configSpecProperties()
  • The crdMinZombieSweeperInterval / crdMinZombieJobTimeout package vars correctly reference agentv1.MinZombieSweeperIntervalSeconds (10) and agentv1.MinZombieJobTimeoutSeconds (30)
  • The test at pkg/k8s_connector_test.go:152 asserts both fields with the correct minimums, matching the admission guards in k8s/apis/agent.benjamin-borbe.de/v1/types.go
  • The structural refactor (configSpecSchemaconfigSpecSchema/configSpecValidations/configSpecProperties) is clean and improves maintainability

The 22 pre-existing go-composition/no-package-function-calls-in-business-logic findings in k8s_connector.go are architectural debt in the existing code, not introduced by this PR.


{
"verdict": "request-changes",
"summary": "The zombie-sweeper CRD fix is correct and well-tested, but two issues from the mechanical funnel need addressing before merge: (1) Listen closes its caller-owned stopCh, violating sender-only channel conventions, and (2) k8s_connector_test.go lacks a Ginkgo suite file causing silent spec non-discovery. A minor naming convention violation on the CRDClientBuilder type alias also warrants a fix.",
"comments": [
{
"file": "pkg/k8s_connector.go",
"line": 121,
"severity": "major",
"message": "close(stopCh) is called inside Listen, but stopCh is received as a parameter from the caller who creates and owns it. Closing from the consumer side races against any concurrent send from the caller. The caller should close stopCh after Listen returns. (rule: go-concurrency/channel-closed-by-sender-only)"
},
{
"file": "pkg/k8s_connector_test.go",
"line": 40,
"severity": "major",
"message": "Ginkgo Describe block found but no companion *_suite_test.go with TestSuite + RunSpecs exists in pkg/. Without it make test exits 0 with zero specs discovered. Create pkg/k8s_connector_suite_test.go. (rule: go-testing/suite-test-file-required)"
},
{
"file": "pkg/k8s_connector_test.go",
"line": 116,
"severity": "major",
"message": "Ginkgo Describe block found but no companion *_suite_test.go with TestSuite + RunSpecs exists in pkg/. Without it make test exits 0 with zero specs discovered. Create pkg/k8s_connector_suite_test.go. (rule: go-testing/suite-test-file-required)"
},
{
"file": "pkg/k8s_connector.go",
"line": 49,
"severity": "nit",
"message": "CRDClientBuilder is a function-type adapter but not named CRDClientBuilderFunc, violating the XxxFunc naming convention. (rule: go-functional-composition/func-type-name)"
}
],
"concerns_addressed": [
"correctness: structural refactor of configSpecSchema into configSpecSchema/configSpecValidations/configSpecProperties — verified, no logic changes, only structural split",
"correctness: new integer minimums (10 and 30) match agentv1 constants (MinZombieSweeperIntervalSeconds=10, MinZombieJobTimeoutSeconds=30) and admission guards in types.go — verified consistent",
"correctness: crdMinZombieSweeperInterval/crdMinZombieJobTimeout reference agentv1 constants — verified, constants exist with correct values"
]
}

Sign up for freeto 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

@bborbe