render

package
v0.5.7 Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package render is SafeLane's render-and-hash seam.

It takes a verified immutable digest, a release target, and an operator-owned Release Template, and produces one github.com/AndrewMaged814/safelane/internal/release.RenderedBundle: the exact final Kubernetes object bytes SafeLane intends to apply, each one content-hashed.

The entire security claim of SafeLane lives in this package. "The bytes that were hashed are the bytes that reach the cluster" is true only if rendering happens once, the hash covers exactly what was rendered, and nothing downstream renders again. Three properties carry that claim, and each is structural rather than a rule someone has to remember.

1. Nothing external supplies the bundle

Render takes a Template loaded from operator-owned files and a release.ReleaseEvidence. It does not take a release.ReleaseRequest, so there is no parameter through which a caller's Kubernetes objects, patches, or template selection could reach rendering - the caller's submission is not in scope here at all. The pinned digest is read from the *verified* evidence, and a release.ReleaseEvidence cannot be constructed outside the release package except through its validating constructor, so rendering against a merely claimed digest is not expressible.

2. The hash covers exactly the bytes that were rendered

Render never hands raw bytes back to a caller alongside a separately computed hash, and never re-parses or re-serializes what it rendered. Every rendered file goes straight from the template executor's buffer into release.NewRenderedResource, which stores the bytes and derives the hash in one step and exposes no way to set them apart. The bundle-level digest is derived from those per-resource hashes rather than from a second serialization of the same objects, so there is only ever one canonical form of the bundle.

The Kubernetes coordinates recorded for each resource (apiVersion, kind, namespace, name) are read back *out of* the rendered bytes rather than taken from template metadata, so the recorded identity describes what was actually produced. That read is for identification only; it never feeds back into the hashed bytes.

3. One rendering, reused everywhere

Render is the only entry point that produces a bundle, and it is a pure function of its arguments. There is no cache keyed by release, no renderer object holding state, and - crucially - no method anywhere on release.Release or release.RenderedBundle that invokes this package. A caller that has a bundle cannot ask for it to be rendered again; it can only read the one it holds. Downstream consumers (proof, execution) receive that value, and release.RenderedResource.Bytes returns a copy, so they cannot alter the bytes the recorded hashes cover.

Calling Render twice with the same Template, target and evidence is not forbidden - it is simply pointless, because it is guaranteed to produce byte-identical output. That is the determinism property, and it is what makes the "single rendering" rule enforceable by comparison rather than by trust.

Determinism

Same template content digest + same verified digest + same target => byte-identical bundle, on any machine, at any time. The mechanism is deliberately boring:

  • text/template over the template files, executed with a *struct* (TemplateData) rather than a map, so there is no map iteration order to leak into output;
  • resource order is the lexicographic order of template file paths, fixed and independent of filesystem iteration order;
  • no template functions are registered at all, so there is no now(), no rand, no uuid, and no env for a template author to reach for;
  • the renderer's signature accepts no clock, no entropy source, and no release.ReleaseID, so a timestamp or an ID cannot appear in rendered bytes even by accident;
  • CRLF is normalized to LF when template files are read, so a Windows checkout and a Linux CI runner produce identical hashes;
  • every value substituted into the output is validated against a strict character set first - which also closes the YAML-injection path that unescaped text/template output would otherwise open.

The fixture template

Ahmed owns the real Release Template (see #47, "Release Template ownership"). Until it exists, this package renders against a fixture under internal/render/testdata/release-template. The fixture is structurally real but has no authority: swapping in the real directory is a drop-in change, and internal/render/testdata/release-template/README.md states exactly what a real template must provide.

Index

Constants

View Source
const (
	// ResourceSuffix marks a file in the Release Template as a resource template.
	// Every such file must render exactly one Kubernetes object.
	ResourceSuffix = ".yaml.tmpl"

	// MetadataFile is the optional Release Template metadata file. It holds
	// "key: value" lines; the recognized keys are "name" and "version". It is
	// optional because the template's content digest, not its label, is what a
	// Release pins.
	MetadataFile = "TEMPLATE"
)

Variables

View Source
var FixtureTemplateFS embed.FS

FixtureTemplateFS is the demo Release Template copied by `safelane setup` into .safelane/release-template when that directory does not exist. Runtime loading uses the operator path in project.yml, not this FS.

Functions

func Render

func Render(t Template, target release.Target, evidence release.ReleaseEvidence, weights []int) (release.RenderedBundle, error)

Render produces the single Rendered Manifest Bundle for one Release.

It is a pure function of its arguments: same template, target and evidence produce byte-identical output every time. It accepts no clock, no entropy, and no release ID, so no non-deterministic value can reach the rendered bytes.

The digest pinned into the bundle is read from the verified evidence, never from a caller's claim. Render fails with "unpinned_template" if the rendered bundle does not contain that digest anywhere, which is the guard that catches a real Release Template whose pod template forgot to reference the image: SafeLane would rather refuse to release than record a bundle that is not pinned to the verified artifact.

weights is the selected lane's rollout envelope (Appendix C3's lanes.<name>.weights), in order. It is the operator's declared lane, resolved once by the caller before rendering -- Render does not pick a lane and does not know what "risk" means. weights must be non-empty: every lane declares at least one weight, its final one, which [DeriveEnvelope] always reconstructs.

func RenderWithOptions added in v0.5.0

func RenderWithOptions(t Template, target release.Target, evidence release.ReleaseEvidence, weights []int, options Options) (release.RenderedBundle, error)

Types

type ContainerPort added in v0.5.3

type ContainerPort struct {
	Name string
	Port int
}

type Options added in v0.5.0

type Options struct{ ProbeImage string }

Options carries operator-owned render inputs that are neither caller intent nor properties of the application artifact.

type ServiceContract added in v0.5.3

type ServiceContract struct {
	Name       string
	Selector   map[string]string
	TargetPort string
}

type TargetContract added in v0.5.3

type TargetContract struct {
	RolloutName     string
	RolloutSelector map[string]string
	PodLabels       map[string]string
	ContainerPorts  []ContainerPort
	StableService   ServiceContract
	CanaryService   ServiceContract
}

TargetContract is the workload and Service shape compiled into a Release Template. Doctor compares this operator-owned contract with the live target before SafeLane declares execution ready.

func InspectTargetContract added in v0.5.3

func InspectTargetContract(t Template, application, namespace string) (TargetContract, error)

InspectTargetContract executes the trusted template with inert deterministic values and reads back only its target-shape fields. It does not create a release bundle or participate in release rendering.

type Template

type Template struct {
	// contains filtered or unexported fields
}

Template is a loaded, operator-owned Release Template.

A Template is immutable once loaded and carries a content digest over every file in the template directory. SafeLane does not author templates and no caller may select or override one: the only way to change what SafeLane renders is to change the operator-owned files, which changes the digest recorded on every subsequent Release.

func LoadDir

func LoadDir(dir string) (Template, error)

LoadDir loads the Release Template from a directory on disk.

func LoadFS

func LoadFS(fsys fs.FS) (Template, error)

LoadFS loads the Release Template from a filesystem.

The content digest covers *every* regular file in the tree, not only the resource templates, because a Release pins its exact template content. An operator who does not want prose edits to change template identity should keep prose out of the template directory.

func (Template) Identity

func (t Template) Identity() release.TemplateIdentity

Identity returns the template identity recorded on every Release rendered from it.

func (Template) IsZero

func (t Template) IsZero() bool

IsZero reports whether the template was never loaded.

func (Template) ResourcePaths

func (t Template) ResourcePaths() []string

ResourcePaths returns the resource template paths in render order.

type TemplateData

type TemplateData struct {
	// Target identity.
	Application string
	Environment string
	Cluster     string
	Namespace   string

	// Verified artifact. ImageReference is the full immutable reference and is what a
	// pod template should use.
	ImageReference  string // ghcr.io/owner/safelane-demo-api@sha256:<hex>
	ImageRegistry   string // ghcr.io
	ImageRepository string // owner/safelane-demo-api
	ImageDigest     string // sha256:<hex>

	// Verified source identity, for traceability annotations. Deterministic: both
	// come from verified evidence, not from a clock.
	SourceRepository string // owner/repo
	SourceRevision   string // merge commit SHA on the base branch
	SourceBranch     string

	// SafeLane-derived resource names. These are conventions, not policy: a real
	// Release Template may hard-code its own names instead. If the pre-created Rollout
	// (#55) uses different names, change the derivation in newTemplateData - the names
	// must match the operator's cluster exactly.
	RolloutName          string // <application>
	StableServiceName    string // <application>-stable
	CanaryServiceName    string // <application>-canary
	AnalysisTemplateName string // <application>-success-rate
	IngressName          string // <application>
	ProbeImage           string // operator-owned digest-pinned black-box probe

	// Steps are the explicit canary steps to render: one setWeight
	// followed by a 60s pause per entry. This is weights[:len(weights)-1]
	// from the selected lane -- the final weight (conventionally 100) is
	// never an explicit step. A canary rollout that runs out of steps
	// promotes fully on its own; that is how Argo Rollouts reaches its
	// last weight, and it is why N configured weights make N-1 gates
	// (Appendix C5), not N.
	Steps []int
}

TemplateData is the complete, explicitly ordered set of values SafeLane substitutes into the Release Template. It is a struct, not a map, so there is no iteration order to leak into rendered bytes and a template typo is an execution error rather than an empty string.

Every field here is derived from the release target or from verified evidence. Nothing on it comes from a caller's free-form input, and nothing on it is time-dependent or random.

A real Release Template may use any subset of these. It must use TemplateData.ImageReference (or TemplateData.ImageDigest) in its pod template, or Render fails - see "unpinned_template" in Render.

Jump to

Keyboard shortcuts

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