ananke/internal/service/testing_hooks_gitops.go

128 lines
6.0 KiB
Go

package service
import (
"context"
"log"
"time"
"scm.bstein.dev/bstein/ananke/internal/config"
"scm.bstein.dev/bstein/ananke/internal/metrics"
)
type TestHookGitOpsRunner func(context.Context, string, ...string) ([]byte, error)
// TestHookParseGitRepositories exposes GitRepository parsing to the top-level
// testing module.
// Signature: TestHookParseGitRepositories(raw []byte) ([]metrics.GitOpsFluxSource, error).
// Why: Ananke keeps split-module tests outside internal packages, but the
// parser needs direct contract coverage without a live cluster.
func TestHookParseGitRepositories(raw []byte) ([]metrics.GitOpsFluxSource, error) {
return parseGitRepositories(raw)
}
// TestHookParseKustomizations exposes Kustomization parsing to the top-level
// testing module.
// Signature: TestHookParseKustomizations(raw []byte) ([]metrics.GitOpsKustomization, error).
// Why: dashboard-facing labels should be verified from representative Flux JSON
// without relying on the current production cluster shape.
func TestHookParseKustomizations(raw []byte) ([]metrics.GitOpsKustomization, error) {
return parseKustomizations(raw)
}
// TestHookParseHelmReleases exposes HelmRelease parsing to the top-level
// testing module.
// Signature: TestHookParseHelmReleases(raw []byte) ([]metrics.GitOpsHelmRelease, error).
// Why: Helm status fields vary by reconciliation phase, and split-module tests
// need a stable seam for parser coverage.
func TestHookParseHelmReleases(raw []byte) ([]metrics.GitOpsHelmRelease, error) {
return parseHelmReleases(raw)
}
// TestHookCollectGitOpsSnapshotWithRunner runs snapshot collection with an
// injected kubectl runner.
// Signature: TestHookCollectGitOpsSnapshotWithRunner(ctx context.Context, cfg config.Config, runner TestHookGitOpsRunner) metrics.GitOpsSnapshot.
// Why: this covers success and partial-failure collection paths without shelling
// out to kubectl from unit tests.
func TestHookCollectGitOpsSnapshotWithRunner(ctx context.Context, cfg config.Config, runner TestHookGitOpsRunner) metrics.GitOpsSnapshot {
original := gitOpsKubectlOutput
defer func() { gitOpsKubectlOutput = original }()
gitOpsKubectlOutput = runner
return collectGitOpsSnapshot(ctx, cfg)
}
// TestHookMaybeStartGitOpsSnapshot starts the daemon's background GitOps scrape
// from split-module tests.
// Signature: TestHookMaybeStartGitOpsSnapshot(ctx context.Context, cfg config.Config, exporter *metrics.Exporter, logger *log.Logger, lastRun *time.Time, running bool, done chan<- struct{}) bool.
// Why: the daemon intentionally keeps scraping asynchronous so GitOps telemetry
// cannot delay UPS polling; the behavior needs direct coverage.
func TestHookMaybeStartGitOpsSnapshot(ctx context.Context, cfg config.Config, exporter *metrics.Exporter, logger *log.Logger, lastRun *time.Time, running bool, done chan<- struct{}) bool {
d := &Daemon{cfg: cfg, exporter: exporter, log: logger}
return d.maybeStartGitOpsSnapshot(ctx, lastRun, running, done)
}
// TestHookMaybeStartGitOpsSnapshotWithRunner starts a background GitOps scrape
// with an injected runner and restores the production runner after completion.
// Signature: TestHookMaybeStartGitOpsSnapshotWithRunner(ctx context.Context, cfg config.Config, exporter *metrics.Exporter, logger *log.Logger, lastRun *time.Time, running bool, done chan<- struct{}, runner TestHookGitOpsRunner) bool.
// Why: the scrape is asynchronous, so split-module tests need a seam that keeps
// fake kubectl behavior installed until the goroutine exits.
func TestHookMaybeStartGitOpsSnapshotWithRunner(ctx context.Context, cfg config.Config, exporter *metrics.Exporter, logger *log.Logger, lastRun *time.Time, running bool, done chan<- struct{}, runner TestHookGitOpsRunner) bool {
original := gitOpsKubectlOutput
gitOpsKubectlOutput = runner
proxyDone := make(chan struct{}, 1)
d := &Daemon{cfg: cfg, exporter: exporter, log: logger}
started := d.maybeStartGitOpsSnapshot(ctx, lastRun, running, proxyDone)
if !started || running {
gitOpsKubectlOutput = original
return started
}
go func() {
<-proxyDone
gitOpsKubectlOutput = original
select {
case done <- struct{}{}:
default:
}
}()
return started
}
// TestHookRunGitOpsKubectlOutput exposes the production command runner to
// split-module tests.
// Signature: TestHookRunGitOpsKubectlOutput(ctx context.Context, name string, args ...string) ([]byte, error).
// Why: stderr preservation and empty-stderr failure behavior are part of the
// operator-facing diagnostics contract.
func TestHookRunGitOpsKubectlOutput(ctx context.Context, name string, args ...string) ([]byte, error) {
return runGitOpsKubectlOutput(ctx, name, args...)
}
// TestHookGitOpsDefaultString exposes defaultString to split-module tests.
// Signature: TestHookGitOpsDefaultString(value string, fallback string) string.
// Why: fallback label behavior is intentionally tiny but important for readable
// Grafana tables during startup.
func TestHookGitOpsDefaultString(value string, fallback string) string {
return defaultString(value, fallback)
}
// TestHookGitOpsFirstNonEmpty exposes firstNonEmpty to split-module tests.
// Signature: TestHookGitOpsFirstNonEmpty(values ...string) string.
// Why: revision fallback order should remain deterministic as Flux status
// payloads change.
func TestHookGitOpsFirstNonEmpty(values ...string) string {
return firstNonEmpty(values...)
}
// TestHookGitOpsErrorSummary exposes gitOpsErrorSummary to split-module tests.
// Signature: TestHookGitOpsErrorSummary(errors map[string]string) string.
// Why: sorted scrape warnings are easier to inspect in journald during recovery.
func TestHookGitOpsErrorSummary(errors map[string]string) string {
return gitOpsErrorSummary(errors)
}
// TestHookFluxReadyMissing exposes the missing Ready-condition branch to tests.
// Signature: TestHookFluxReadyMissing() (bool, string).
// Why: absent Ready conditions should be treated as not-ready and labelled
// explicitly instead of being mistaken for healthy.
func TestHookFluxReadyMissing() (bool, string) {
return fluxReady(nil)
}