teamrender

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 12, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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

View Source
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).

View Source
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.

View Source
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.

View Source
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`.

View Source
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

func MarshalYAML(res *Result) ([]byte, error)

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

func MergeNeeds(a, b []string) []string

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

type ReportLine struct {
	OK     bool
	Detail string
}

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:

  1. 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).
  2. templates — for every selected harness, does harness.Spec.Template resolve under the materialized cache (relative to the harness dir, per #1110)?
  3. partials — parse every harness's blueprint template set; for every `{{ template "name" }}` invocation, does a matching `{{ define }}` exist in the harness's partial set?
  4. 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL