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
- Variables
- func Render(t Template, target release.Target, evidence release.ReleaseEvidence, ...) (release.RenderedBundle, error)
- func RenderWithOptions(t Template, target release.Target, evidence release.ReleaseEvidence, ...) (release.RenderedBundle, error)
- type ContainerPort
- type Options
- type ServiceContract
- type TargetContract
- type Template
- type TemplateData
Constants ¶
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 ¶
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 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 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 LoadFS ¶
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) ResourcePaths ¶
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.