Documentation
¶
Overview ¶
Package repo implements the decentralized, Helm-style module distribution client described in docs/design/module-distribution-v2.md: repos are static index files, the source of truth is the module repo + its signed image, and the platform is demoted to an optional aggregator.
This package is the client foundation — repo config, index fetch/cache, and resolution. It does NOT yet replace internal/catalog or the install path; wiring install/up onto it is a later phase (kept separate so the working install keeps working).
Index ¶
- Constants
- func Major(version string) (int, error)
- func MarshalIndex(idx *Index, generated string) ([]byte, error)
- func ReleaseName(repo, module string, major int) string
- func RenderValues(raw []byte, fills map[string]string) (map[string]any, error)
- type BaseValues
- type Bundle
- type BundlePlan
- type Config
- type Helm
- type Index
- type InstallPlan
- type InstalledModules
- type Installer
- type Manifest
- type Merged
- type Module
- type ModuleSummary
- type Repo
- type Resolved
- type Store
- type Version
Constants ¶
const ( // DefaultRepoName is the baked-in repo added on first run. Removable. DefaultRepoName = "tinysystems" // DefaultRepoURL is the baked-in default index — the tiny-systems/modules // repo, served raw off main. (A prettier GitHub Pages URL can replace this // once Pages is enabled; the raw URL is a plain-GET static file either way.) DefaultRepoURL = "https://raw.githubusercontent.com/tiny-systems/modules/main/index.yaml" )
const APIVersion = "tiny/v2"
APIVersion is the index schema version this client understands.
const ManifestFile = "module.yaml"
ManifestFile is the filename a module repo publishes to describe itself for the index generator. It is the transparent, git-visible source the generator aggregates (alongside — eventually — OCI image annotations).
Variables ¶
This section is empty.
Functions ¶
func Major ¶
Major parses the SemVer major from a version string ("2.3.1" → 2, "v0.5.24" → 0, "1.0.0-rc1" → 1). The major is the coexistence coordinate: two majors of a module run side by side in the same namespace, so it drives the release name and every K8s resource identity. See docs/design §7.
func MarshalIndex ¶
MarshalIndex renders an index to YAML, stamping apiVersion and (when given) a generated timestamp. The timestamp is passed in rather than read from the clock so callers stay deterministic/testable.
func ReleaseName ¶
ReleaseName is the coexistence-safe helm release / resource identity: `<repo>-<module>-v<major>`. Two coordinates are derived at install time, so both dimensions can coexist in one namespace (distinct releases, labels, node refs):
- major — v1 and v2 of the same module (design §7)
- repo — the same module name published by different people (§7.1). A cluster holds modules from many publishers and nothing stops two of them shipping "http-module"; without this the second install silently replaces the first.
repo is the LOCAL repo name from repos.yaml, not the GitHub org — identity stays under the cluster operator's control and survives an upstream rename. It may contain dashes (tiny-systems does); this string is an opaque identity and is never split back into its parts. Two pairs can therefore generate one name ("a-b"+"c" and "a"+"b-c"), which the installer catches by comparing the authoritative repo/module labels rather than by restricting names.
An empty repo yields the legacy `<module>-v<major>` form, so callers that don't know the publisher keep working.
func RenderValues ¶
RenderValues fills a module's raw values.yaml holes (`${cluster.<name>}`) from the resolved cluster settings and parses the result. An unresolved hole is an error, not a silent blank — a half-filled values file must never reach helm.
Types ¶
type BaseValues ¶
BaseValues builds the harness (operator-chart) values every module install needs, from the plan's identity fields. provision.BaseValues satisfies it — injected so this package stays free of the helm/chart dependency graph.
type Bundle ¶
type Bundle struct {
Name string `json:"name"`
Chart string `json:"chart"` // oci://… or repo/chart
ChartVersion string `json:"chartVersion,omitempty"`
DefaultOn bool `json:"defaultOn,omitempty"`
}
Bundle is an optional third-party Helm release a module offers (§3.5).
type BundlePlan ¶
type BundlePlan struct {
Name string
Chart string
ChartVersion string
ReleaseName string // <moduleRelease>-<bundle>
}
BundlePlan is one bundle's install — its own helm release next to the module.
type Config ¶
type Config struct {
Repos []Repo `json:"repos"`
}
Config is the client-side repo list, persisted at ${XDG_CONFIG_HOME:-~/.config}/tiny/repos.yaml.
func LoadConfig ¶
LoadConfig reads repos.yaml, seeding the default repo when the file is absent. A present-but-empty file is respected (the user removed the default).
type Helm ¶
type Helm interface {
// UpgradeInstall installs or upgrades release in namespace from chart at
// version (a range or exact), with the given merged values.
UpgradeInstall(ctx context.Context, release, namespace, chart, version string, values map[string]any) error
}
Helm is the subset of helm the installer needs. provision.Client will satisfy it via a thin adapter at cutover time; tests fake it. Keeping the executor behind this interface is what makes install planning testable without a cluster and keeps this package free of the helm dependency graph.
type Index ¶
type Index struct {
APIVersion string `json:"apiVersion"`
Generated string `json:"generated,omitempty"`
Modules map[string]*Module `json:"modules"`
}
Index is a repo's catalog — a static file served at the repo URL. See docs/design/module-distribution-v2.md §4.1.
func BuildIndex ¶
BuildIndex assembles a tiny/v2 index from module manifests, sorting each module's versions newest-first (the invariant Resolve.latest relies on). Duplicate module names are an error — a module belongs to exactly one repo.
func GenerateFromDir ¶
GenerateFromDir walks dir for module.yaml files and builds the index. One manifest per file; nested layout (dir/<module>/module.yaml) is fine.
func ParseIndex ¶
ParseIndex decodes and lightly validates an index document (YAML or JSON — sigs.k8s.io/yaml handles both).
type InstallPlan ¶
type InstallPlan struct {
ReleaseName string // <module>-v<major> — coexistence-safe
Namespace string //
Version string // SemVer (for the operator's --version)
Image string // ghcr ref
Digest string // for cosign/digest verification
Chart string // harness chart name
ChartVersion string // compatible range
Cosign bool // verify before install
Fills map[string]string // cluster holes the module asked for → values
MissingFills []string // holes the module needs but the cluster didn't provide
Bundles []BundlePlan // extra helm releases to install alongside
}
InstallPlan is the concrete description of what installing a resolved module entails. It is produced with no side effects (no helm, no network), so it can be shown and confirmed before anything runs. The actual values-file fetch + hole substitution + helm calls are the installer's job (a later phase); this hands it every coordinate it needs.
func Install ¶
func Install( ctx context.Context, merged *Merged, ref, namespace string, clusterSettings map[string]string, selectedBundles []string, base BaseValues, helm Helm, installed InstalledModules, ) (*InstallPlan, error)
Install runs the full pipeline for one module reference:
resolve → find the module version across the merged index
plan → derive release name (<repo>-<module>-v<major>), image, fills, bundles
reconcile → settle that name against what is already installed: adopt a
legacy pre-publisher release, refuse another publisher's
values → harness base (identity/plumbing) ⊕ module overlay (rbac/ingress/…),
the overlay's ${cluster.*} holes filled and winning on conflicts
apply → helm-install the module release + each bundle
The module's values.yaml is inlined in the index (self-contained/offline). base + helm + installed are injected so this stays the pure orchestration seam. installed may be nil (fresh-cluster semantics: no adoption, no guard).
type InstalledModules ¶ added in v0.4.41
type InstalledModules interface {
// Lookup returns the module image ref of whatever occupies release in
// namespace (`ghcr.io/<org>/<module>:<tag>`) — the authoritative answer to
// "whose module is this?", since it is literally what the pod runs. Returns
// found=false when no such release exists, and an empty image when the
// release exists but its image could not be read.
//
// Primitives, not a struct, so implementations satisfy this structurally
// without importing this package — same pattern as BaseValues and Helm.
Lookup(ctx context.Context, namespace, release string) (image string, found bool, err error)
}
InstalledModules reports what already occupies a release name in the namespace. It exists for the two things a rename cannot do without looking first (design §7.1):
- ADOPT — a cluster installed before the publisher coordinate holds `http-module-v0`. Installing the newly-computed `tinysystems-http-module-v0` would stand a second copy beside it while every existing flow node stays bound to the old name. Adopting the legacy release keeps those flows working and makes the change a non-event.
- GUARD — publisher names may contain dashes, so two different pairs can generate one release name ("a-b"+"c" and "a"+"b-c"). Installing over it would silently replace someone else's module.
Optional: nil skips both (fresh-cluster semantics, and what unit tests use).
type Installer ¶
type Installer struct {
// contains filtered or unexported fields
}
Installer applies an InstallPlan: the module's harness release plus one helm release per selected bundle. It performs no resolution or planning — hand it a plan and the module's rendered values.
func NewInstaller ¶
NewInstaller returns an installer over a Helm backend.
type Manifest ¶
Manifest is one module repo's self-description (its `module.yaml`): the module name plus everything an index entry carries. The embedded Module fields (source, description, category, versions) are inlined into the file.
func ParseManifest ¶
ParseManifest decodes a module.yaml.
type Merged ¶
type Merged struct {
// contains filtered or unexported fields
}
Merged is a read-only view over every configured repo's index, ordered so resolution is deterministic. Repo names are unique (config enforces it).
func NewMerged ¶
NewMerged builds a merged view. order fixes precedence for ambiguity reporting (not silent shadowing — ambiguity is an error, see Resolve).
func (*Merged) List ¶
func (m *Merged) List() []ModuleSummary
List returns every available module across the merged indexes, sorted by name — the discovery scan layer (get_module_readme pulls the detail).
func (*Merged) Resolve ¶
Resolve finds a module version across the merged indexes. Accepted forms:
module module[@version] searched across all repos repo/module module in that specific repo module@1.2.3 a specific version repo/module@1.2.3
A bare name present in more than one repo is an error (name the repos); a name absent everywhere retries with the legacy `-v0` alias in both directions (http-module ⇄ http-module-v0) to bridge the migration.
type Module ¶
type Module struct {
Source string `json:"source,omitempty"` // github.com/org/repo — also where the README lives
Description string `json:"description,omitempty"`
Category string `json:"category,omitempty"`
Versions []*Version `json:"versions"`
}
Module is one module's entry: identity, presentation, and its versions. Description is the one-line summary (lists). The full markdown doc is NOT inlined — it's the module repo's own README, fetched on demand from Source (which is the doc's source of truth). See ReadmeURL.
func (*Module) ReadmeURL ¶
ReadmeURL derives the raw README URL from a module's Source (github.com/org/repo → raw.githubusercontent.com/org/repo/HEAD/README.md). Returns "" if Source isn't a github.com coordinate. The platform/agent fetches this on demand for detail views — install never needs it, so it stays online- only while install data stays offline-cached.
type ModuleSummary ¶
type ModuleSummary struct {
Repo string `json:"repo"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Category string `json:"category,omitempty"`
Source string `json:"source,omitempty"`
LatestVersion string `json:"latestVersion,omitempty"`
ReadmeURL string `json:"readmeURL,omitempty"`
}
ModuleSummary is the limited, cheap-to-scan info an agent uses to shortlist a module before fetching its full README. No versions/values/schemas — just enough to decide "is this worth a closer look?".
type Resolved ¶
Resolved is a fully-resolved install target.
func (*Resolved) Plan ¶
func (r *Resolved) Plan(namespace string, clusterSettings map[string]string, selected []string) (*InstallPlan, error)
Plan turns a resolved target into an InstallPlan. clusterSettings supplies values for the module's cluster holes (ingressClass, storageClass, …); selected chooses bundles: nil → module defaults, ["none"] → zero, else exactly those named. It executes nothing.
func (*Resolved) ReleaseName ¶
ReleaseName derives the release name for a resolved target from its repo + module name + the version's SemVer major.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store manages the configured repos and their cached indexes. It is the entry point the CLI and (later) the installer use.
func (*Store) Merged ¶
Merged reads the cached indexes and returns a merged, resolvable view. Repos with no cached index yet (never updated) are skipped; the caller can suggest `tiny repo update`.
type Version ¶
type Version struct {
Version string `json:"version"`
Image string `json:"image"`
Digest string `json:"digest,omitempty"`
Chart string `json:"chart,omitempty"` // harness chart name
ChartVersion string `json:"chartVersion,omitempty"` // compatible range
Values string `json:"values,omitempty"` // inline values.yaml (with ${cluster.*} holes)
ClusterFills []string `json:"clusterFills,omitempty"` // holes tiny fills
Bundles []Bundle `json:"bundles,omitempty"`
Cosign bool `json:"cosign,omitempty"` // image + entry are signed
}
Version is one installable release: the image + the values/chart coordinates and optional bundles needed to install it.