Documentation
¶
Overview ¶
Package teamrender renders a project's ProjectTeam roster into a set of CellBlueprint and CellConfig documents — one (CellBlueprint, CellConfig) pair per `(role × harness)` (epic #792, step 3 #1042). For each pair the pipeline:
- needs-merge — `union(role.yaml.needs.image ⊕ kuketeam per-role needs.image)`, deduplicated and lexicographically sorted so two runs against the same inputs produce byte-identical output.
- image-select — match the merged capability set against the agents source's ImageCatalog, picking the first entry whose `harness` matches and whose `capabilities` superset the merged needs. A miss surfaces errdefs.ErrTeamImageNoMatch naming the first unmet capability + the operator-actionable "build/label an image" hint.
- render — load the harness's blueprint template (relative to the harness's own directory under the materialized cache, see #1110), parse it as a Go text/template against a typed dot-context (`.role`, `.harness`, `.needs`, `.harnesses`, `.operator`, `.project`, `.image`, `.realm`/`.space`/`.stack`), pull in every sibling `*.tmpl.yaml` partial in the same dir so `{{ template "name" . }}` calls resolve against them, execute, and yaml-parse the result into a CellBlueprintDoc. Per the umbrella's key decision the role's native per-harness config is wired verbatim — no transpile.
- bind — produce a CellConfig that references the just-rendered blueprint and carries (a) operator facts pulled from `~/.kuke/kuketeams.yaml` (git author/committer/signingKey/registry stamped into Values), (b) the project's cloned repo URL stamped into the `project` repo slot fill, and (c) for every secret the role declares it needs and the blueprint declares a slot for, an in-realm ContainerSecretRef pointing at the Secret of the same name (created out of `secrets.env`, #1120).
- label — every rendered CellBlueprint and CellConfig carries `metadata.labels[kukeon.io/team] = <project>` so the daemon-side prune-apply machinery from #1029 can converge the project's slice in step 4 (#1043) without touching other teams' objects.
The package writes nothing to disk and runs no external commands — it reads the materialized template files prepared by teamsource (#1041) and produces in-memory v1beta1 documents. `--dry-run` consumers marshal the Result to YAML; the apply path in step 4 (#1043) hands the same Result straight to ApplyDocumentsForTeam.
Index ¶
- Constants
- func BindConfig(bp *v1beta1.CellBlueprintDoc, r *model.Role, roleRef, harness string, ...) *v1beta1.CellConfigDoc
- func MarshalYAML(res *Result) ([]byte, error)
- func MergeNeeds(a, b []string) []string
- func RenderBlueprint(cacheDir string, h *model.Harness, r *model.Role, harness, roleRef string, ...) (*v1beta1.CellBlueprintDoc, error)
- func SelectImage(ic *model.ImageCatalog, harness string, needs []string) (*model.ImageCatalogEntry, error)
- type Inputs
- type ReportLine
- type ReportSection
- type Result
- type ValidationReport
Constants ¶
const ( DefaultSpace = "default" DefaultStack = "default" )
DefaultSpace and DefaultStack are the space/stack rendered objects bind to when Inputs.Space / Inputs.Stack are empty. They mirror DefaultRealm and the `default/default/default` coordinates the CLI create path defaults an omitted scope to (cmd/kuke/run/run.go). Defaulting all three here — rather than copying the (usually empty) template values — keeps the rendered Config's scope identical to the live cell's persisted scope, so the reconciler's re-materialization (internal/cellconfig/materialize.go) produces matching spec.spaceName / spec.stackName and DiffCell reports no spurious OutOfSync (#1133).
const ( SectionCatalog = "catalog" SectionTemplates = "templates" SectionPartials = "partials" SectionFacts = "facts" )
Section titles for the validate report. Exported so consumers (and tests) can key off the canonical names rather than string literals.
const AgentsRepoSlotName = "agents"
AgentsRepoSlotName mirrors ProjectRepoSlotName for the pinned agents source clone. `BindConfig` fills it from the operator's `TeamsConfig.spec.sources[<owner/repo>]` mapping when the blueprint declares the slot.
const DefaultRealm = "default"
DefaultRealm is the realm rendered objects bind to when Inputs.Realm is empty. Matches the `default` user realm provisioned by `kuke init` (see internal/consts) — team workloads live in the user realm, not in `kuke-system`.
const ProjectRepoSlotName = "project"
ProjectRepoSlotName is the convention for the structural repo slot that carries the project's clone URL. The umbrella (epic #792) names `project` and `agents` as the two repos a team cell typically clones; this package owns the contract that blueprint templates declare the slot with this name, and `BindConfig` fills it from `Inputs.ProjectRepoURL`.
Variables ¶
This section is empty.
Functions ¶
func BindConfig ¶
func BindConfig( bp *v1beta1.CellBlueprintDoc, r *model.Role, roleRef, harness string, tc *model.TeamsConfig, src teamsource.Source, in Inputs, project, realm, space, stack string, ) *v1beta1.CellConfigDoc
BindConfig produces a CellConfig referencing bp. The operator-fact values (git author/committer/signingKey/allowedSigners/registry) land in Values keyed by the same `${KEY}` names the blueprint may declare as CellBlueprintParameters; the daemon resolves them at run time per the CellConfig contract. The project's clone URL fills the `project` repo slot; the agents source URL (from tc.spec.sources[<owner/repo>]) fills the `agents` slot — both only when the blueprint actually declares the slot, so a template that doesn't carry the slot produces a config without a stray fill. The secret-slot fills are sourced from the role's per-harness list (role.spec.harnesses[harness].secrets, the agents#750 location) merged with the role-level fallback (role.spec.needs.secrets) — see effectiveSecretNames. Each declared secret name is matched against the blueprint's BlueprintSecretSlot declarations and, when the blueprint declares the slot, filled with an in-realm ContainerSecretRef pointing at the secret of the same name — the runtime resolves the actual bytes via the Secret kind (#623) created out of the two-layer secrets.env path (#1120).
func MarshalYAML ¶
MarshalYAML returns the Result as a single multi-document YAML stream. Bundle order: every Secret, then every Blueprint, then every Config — the "Secrets → Blueprints → Configs" ordering called out in #1113. The per-section order matches the slice order on Result (Secrets sorted by metadata.name by teamsecrets.Render; Blueprints/Configs in (role × harness) iteration order) so the dry-run output and the apply-bundle payload are both deterministic.
func MergeNeeds ¶
MergeNeeds returns the lexicographically sorted union of a and b. Empty and whitespace-only entries are dropped, so a per-role override that repeats a role.yaml capability yields a single entry rather than a duplicate. The output is a fresh slice — callers may mutate it without affecting the inputs.
func RenderBlueprint ¶
func RenderBlueprint( cacheDir string, h *model.Harness, r *model.Role, harness, roleRef string, needs []string, image *model.ImageCatalogEntry, tc *model.TeamsConfig, src teamsource.Source, in Inputs, project, realm, space, stack string, ) (*v1beta1.CellBlueprintDoc, error)
RenderBlueprint loads the harness's blueprint template, parses it as a Go text/template (alongside every sibling `*.tmpl.yaml` partial in the same directory), executes it against the typed dot-context the agents repo's published blueprints are authored against, and yaml-parses the result into a CellBlueprintDoc.
Template path resolution: harness.Spec.Template resolves relative to the harness's own directory (`<cacheDir>/harnesses/<harness>/`), not the cache root. The agents repo's `harnesses/<name>/harness.yaml` declares `template: blueprint.tmpl.yaml` as a sibling, so the bare-filename form is the canonical shape.
Sibling partials: every `*.tmpl.yaml` in the resolved template's directory is parsed into the same `*template.Template` set so a blueprint's `{{ template "mount_source" . }}` call resolves against a `{{ define "mount_source" }}…{{ end }}` block in any sibling (`partials.tmpl.yaml`, `mounts.tmpl.yaml`, …) without the renderer owning a partials directory of its own.
Dot-context: see renderContextValues for the full shape. The `.operator.*` and `.project.*` leaves are bound from tc, src, and in: the agents source's owner/repo path drives `.operator.REPO_OWNER` (when no tc.spec.repoOwner override is set) and `.project.AGENTS_REPO`; in.ProjectDir and in.TeamDir surface as `.project.PROJECT_DIR` and `.operator.TEAM_ROOT` respectively; tc.spec.homeDir (or `$HOME` when unset) fills `.operator.HOME_DIR`. The metadata.labels are populated with `kukeon.io/team = project`. metadata.realm/space/stack are forced to the (defaulted) realm/space/stack scope (the template need not pre-fill them) so the rendered Config records the same explicit scope the live cell persists — closing the defaulting asymmetry behind the spurious OutOfSync in #1133. If the template did not supply metadata.name, the default `<role>-<harness>` is stamped so the blueprint and its companion config share a deterministic identity.
in.Build + in.SourceRef drive the `.image` bind decision (see renderContextValues): in build mode the locally-built `kukeon.internal/<ref>:<sourceRef>` image is bound; otherwise the catalog entry's published Image is.
func SelectImage ¶
func SelectImage( ic *model.ImageCatalog, harness string, needs []string, ) (*model.ImageCatalogEntry, error)
SelectImage picks the ImageCatalog entry whose `harness` matches and whose `capabilities` superset every entry in needs. Catalog order is the tiebreaker — the first matching entry wins. A miss surfaces errdefs.ErrTeamImageNoMatch naming the first capability no harness-matching image provides + the operator-actionable hint. Empty needs match any image carrying the requested harness; an empty catalog is always a miss (the operator must populate images.yaml).
Types ¶
type Inputs ¶
type Inputs struct {
Project string
ProjectRepoURL string
// ProjectDir is the on-host directory the project's kuketeam.yaml was read
// from (composeTeam's os.Getwd()). Exposed to blueprint templates as
// `.project.PROJECT_DIR` so the operator can reference the source-tree
// path without bouncing it through a CellConfig parameter.
ProjectDir string
// ProjectCloneDir overrides the in-cell project clone dir basename the
// renderer exposes as `.project.NAME` (the fact a blueprint's `project` repo
// target reads for its `/home/<user>/<dir>` clone path). Sourced from the
// project's kuketeam.yaml spec.projectDir; empty falls back to Project (the
// team label). Distinct from ProjectDir above — that is the on-host
// source-tree path (`.project.PROJECT_DIR`), this is the in-cell clone dir.
// Decoupling the two breaks the self-referential-team collision (#1166)
// where the `project` and `agents` slots otherwise both target
// `/home/<user>/agents`.
ProjectCloneDir string
// TeamDir is the per-team host-state root resolved by composeTeam
// (TeamEntry.spec.teamDir override, else Layout.TeamDir(team)). Exposed to
// blueprint templates as `.operator.TEAM_ROOT`. Per-team-scoped: two teams
// running on the same operator host see two different TEAM_ROOT values.
TeamDir string
// Realm, Space, and Stack are the scope coordinates the rendered
// Blueprints/Configs bind to. Each defaults to `default` when empty (see
// DefaultRealm / DefaultSpace / DefaultStack) so the rendered Config
// records an explicit scope matching the live cell the CLI create path
// persists, closing the defaulting asymmetry that caused spurious
// OutOfSync (#1133). Sourced from the project's kuketeam.yaml
// (ProjectTeamSpec) when declared.
Realm string
Space string
Stack string
// Build is true under `kuke team init --build`: the rendered blueprint
// binds the locally-built `kukeon.internal/<ref>:<version>` image (the tag
// teambuild produces) instead of the catalog entry's published `Image`.
// SourceRef supplies the `<version>` tag suffix — the agents source's
// pinned ref — so the bound ref matches the built tag byte-for-byte. When
// Build is false (the default), the catalog's published Image is bound.
Build bool
SourceRef string
}
Inputs collects the per-project facts that vary by `kuke team init` invocation. Project is the rendered objects' team label and the basis for the blueprint/config name when the template did not supply one. ProjectRepoURL is the clone URL for the project repo — typically read from `git -C <projectDir> remote get-url origin` by the caller.
type ReportLine ¶
ReportLine is one outcome row in a validate section. OK distinguishes a satisfied check from a gap; Detail carries the human-readable body the report writer prints after the `ok`/`miss` status column.
type ReportSection ¶
type ReportSection struct {
Title string
Lines []ReportLine
}
ReportSection groups the outcome lines for one of the four contract checks. Title is one of the Section* constants.
type Result ¶
type Result struct {
Secrets []*v1beta1.SecretDoc
Blueprints []*v1beta1.CellBlueprintDoc
Configs []*v1beta1.CellConfigDoc
Selections []*model.ImageCatalogEntry
}
Result carries the rendered objects from one project's roster. Each CellBlueprint at index i has its companion CellConfig at the same index in Configs, simplifying the apply loop in step 4 (#1043). Selections carries the ImageCatalog entry that satisfied each (role × harness) pair at the same index — `kuke team init --build` hands this slice to teambuild.BuildAll to drive the local build path (#1064). The slice is deduplicated by entry.Ref so a catalog entry reused across multiple (role × harness) pairs builds once.
Secrets is the merged shared+per-team secret set composed by the teamsecrets package (#1113) and is included in the apply payload by MarshalYAML before Blueprints and Configs — the apply bundle order is Secrets → Blueprints → Configs so a Blueprint that references a Secret via ContainerSecret.secretRef sees a daemon-side record present at reconcile time.
func Render ¶
func Render( bundle *teamsource.Bundle, pt *model.ProjectTeam, tc *model.TeamsConfig, in Inputs, ) (*Result, error)
Render runs the full per-(role × harness) pipeline against a loaded Bundle. The outer loop is project-deterministic: roles are visited in the order pt.Spec.Roles declares them and harnesses in the order pt.Spec.Defaults.Harnesses declares them, so the same inputs always produce the same output ordering.
type ValidationReport ¶
type ValidationReport struct {
Sections []ReportSection
}
ValidationReport is the full one-pass contract check produced by Validate: four sections (catalog, templates, partials, facts), each carrying its ok/miss lines in a stable order. Write renders it; HasMiss / MissCount drive the caller's exit code.
func Validate ¶
func Validate( bundle *teamsource.Bundle, pt *model.ProjectTeam, ) *ValidationReport
Validate runs the one-pass contract check across a resolved Bundle and a project roster. Unlike Render — which fails fast on the first catalog miss or template error — Validate visits every (role × harness) pair and every selected harness, collecting all gaps into a single report so an operator fixes the whole drift in one pass rather than one error per re-run.
The four sections mirror the render pipeline's failure surfaces:
- catalog — for every (role × harness), does SelectImage find a match? ok lines name the selected catalog ref; miss lines name the first unmet capability (or the no-single-image case).
- templates — for every selected harness, does harness.Spec.Template resolve under the materialized cache (relative to the harness dir, per #1110)?
- partials — parse every harness's blueprint template set; for every `{{ template "name" }}` invocation, does a matching `{{ define }}` exist in the harness's partial set?
- facts — for every `{{ .operator.X }}` / `{{ .project.X }}` reference in any template body, does the render dot-context expose the key? (Checked against the known fact schema, not the populated subset — an unset-but-known fact renders empty, not a gap.)
A nil or empty bundle (the harness-less roster) yields four empty sections and no misses — there is nothing to validate. Validate writes nothing to disk beyond reading the already-materialized template files, and never returns an error for a gap: gaps live in the report (HasMiss / MissCount).
The fact check is value-independent — it compares referenced keys against the dot-context's key *schema*, not an operator's populated subset — so Validate needs neither the TeamsConfig nor the per-run Inputs that Render consumes.
func (*ValidationReport) HasMiss ¶
func (r *ValidationReport) HasMiss() bool
HasMiss reports whether any section carries at least one miss line.
func (*ValidationReport) MissCount ¶
func (r *ValidationReport) MissCount() int
MissCount returns the total number of miss lines across every section.
func (*ValidationReport) Write ¶
func (r *ValidationReport) Write(w io.Writer) error
Write renders the report to w as the multi-section gap report. Each section is headed by `== <title> ==` and each line by a fixed-width `ok`/`miss` status column, so the catalog/templates ok lines and the partials/facts miss lines line up regardless of body width. Sections are separated by a blank line; all four section headers are always emitted (a clean section is just its header) so the operator can tell a passed check from a skipped one.