specwizard

package
v0.33.0 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package specwizard implements the conversion layer between knot's native template spec formats (local container YAML and Nomad HCL) and the unified representation the web wizard edits.

The wizard never reads or writes native specs directly; it always goes via Parse→UnifiedSpec→Build. Build is source-position-aware: it patches only the fields the wizard knows about in the original native text, preserving anything outside that surface byte-for-byte.

Index

Constants

View Source
const (

	// DefaultUpdateURL is the fallback manifest URL used when no explicit
	// --base-images-update-url is configured and no manifest file is in use.
	DefaultUpdateURL = "https://getknot.dev/base-images.toml"

	// Source tags recorded against a fetched manifest for diagnostics.
	SourceStartup = "startup"
	SourceAPI     = "api"
)

Variables

This section is empty.

Functions

func ActiveManifestVersion

func ActiveManifestVersion() string

ActiveManifestVersion returns the revision of the manifest LoadManifest would currently return: the newer of the baseline (external file if configured, else embedded) and any fetched overlay.

func Build

func Build(platform string, spec *apiclient.UnifiedSpec, originalJob, originalVolumes string) (job, volumes string, err error)

Build dispatches to the platform-specific builder. originalJob / originalVolumes are the current native spec text; both platforms now patch their Volume Definition YAML in place rather than regenerating it wholesale.

func BuildContainerYAML

func BuildContainerYAML(spec *apiclient.UnifiedSpec, originalJob, originalVolumes string) (job, volumes string, err error)

BuildContainerYAML converts a UnifiedSpec back into container YAML plus the Volume Definition YAML. When originalJob is non-empty, the wizard's fields are patched into the original text via yaml.Node (preserving comments on scalar fields and field ordering for lines the wizard doesn't touch). When originalJob is empty, a fresh YAML is generated from the spec. The Volume Definition side (originalVolumes) is always patched independently via buildLocalStorageDefinitions, regardless of which path the job text takes.

If the original text is valid YAML that can't be patched safely (multiple documents, anchors), Build errors instead of regenerating — regeneration would drop the parts of the file the wizard doesn't model. Text that isn't valid YAML at all carries nothing worth preserving, so it's replaced.

func BuildNomadHCL

func BuildNomadHCL(spec *apiclient.UnifiedSpec, originalHCL string, originalVolumes string) (job, volumes string, err error)

BuildNomadHCL produces HCL for a Nomad job from the unified spec.

If originalHCL is empty, a fresh job skeleton is emitted using the standard variable placeholders (${{ .space.name }} etc.) that all Nomad templates in the knot catalog use.

If originalHCL is non-empty, the wizard-controlled fields are patched into the original text via constrained regex on the first task block. Fields the wizard doesn't know about (constraints, meta, services, multicleaf stanzas) are preserved byte-for-byte. Comments outside patched ranges survive.

func Capabilities

func Capabilities() []apiclient.CapabilityEntry

Capabilities returns the wizard's capability catalog. The slice is copied so callers (including the API handler) can't mutate the package-level catalog.

func CheckFullyRepresentable

func CheckFullyRepresentable(platform, job, volumes string, spec *apiclient.UnifiedSpec) (fully bool, reason string)

CheckFullyRepresentable inspects a successfully-parsed spec and its raw source texts to determine whether the wizard can display everything. It returns fully=true when the wizard covers every field; otherwise it returns false with a human-readable reason listing what was detected outside the wizard's surface.

This is separate from the wizardable check (which refuses to parse at all for multi-task jobs etc.) — a spec can be wizardable (parseable, editable) without being fully representable (the user would miss things if they only used wizard mode).

func EmbeddedManifestVersion

func EmbeddedManifestVersion() string

EmbeddedManifestVersion returns the catalog revision compiled into the binary.

func FetchDecision

func FetchDecision(cfg *config.ServerConfig) (url string, ok bool)

FetchDecision decides whether a manifest fetch should happen and from what URL, based on the server configuration. The startup fetch and the manual refresh follow the SAME rule, governed by --base-images-update-enabled as a master gate: when it is off, no fetch happens at all (neither startup nor manual); when it is on, a fetch happens subject to the manifest/url rules.

Notation: M = base-images-manifest set, A = base-images-update-enabled on, U = base-images-update-url set.

Fetch iff A && (!M || U)

URL: the configured --base-images-update-url when set (U); otherwise the DefaultUpdateURL — but the default is only ever reached when no manifest file is configured (M=false), because the only M-set case that fetches requires U.

func FetchDisabledReason

func FetchDisabledReason(cfg *config.ServerConfig) string

FetchDisabledReason returns a human-readable explanation of why FetchDecision returned ok=false for the given config, for use in error messages.

func FetchOnStartup

func FetchOnStartup()

FetchOnStartup fetches the manifest once, in the background, per the startup FetchDecision. It returns immediately; success or failure is only logged. There is no periodic loop — the catalog only changes on startup or via an explicit refresh (RefreshNow / the admin CLI).

func FetchedManifestSource

func FetchedManifestSource() string

FetchedManifestSource returns where the active fetched manifest came from ("startup", "api"), or "" when the embedded/external manifest is active.

func NormaliseCapabilities

func NormaliseCapabilities(in []string) []string

NormaliseCapabilities canonicalises a list of capability names, dropping blanks and malformed entries and de-duplicating while preserving the caller's order. Returns nil for an empty result so emitters omit the field entirely.

func NormaliseCapability

func NormaliseCapability(name string) string

NormaliseCapability converts a capability name into the canonical CAP_UPPER_SNAKE form used inside UnifiedSpec. Accepts the bare form (net_admin), mixed case, surrounding whitespace and the already-canonical form. Returns "" when the name isn't a well-formed capability so callers can drop it rather than writing garbage into a spec.

func Parse

func Parse(platform, job, volumes string, hclParser HCLParser) (*apiclient.UnifiedSpec, bool, string)

Parse dispatches to the platform-specific parser. It returns wizardable=false for unknown platforms so the caller can communicate that to the UI.

For Nomad, an HCLParser must be supplied (typically wrapping a Nomad client). A nil parser for the Nomad platform is treated as "Nomad not configured" and returns wizardable=false.

func ParseContainerYAML

func ParseContainerYAML(job, volumes string) (spec *apiclient.UnifiedSpec, wizardable bool, reason string)

ParseContainerYAML converts a local container YAML spec into UnifiedSpec. wizardable is false when the YAML can't be decoded, or when it uses constructs the comment-preserving patcher can't round-trip safely (multiple documents, anchors/aliases). The wizard UI uses that flag to keep the user in the raw editor rather than risk rewriting a spec it doesn't fully understand.

func ParseNomadHCL

func ParseNomadHCL(job, volumes string, parser HCLParser) (spec *apiclient.UnifiedSpec, wizardable bool, reason string)

ParseNomadHCL converts a Nomad HCL job into UnifiedSpec. The HCL must be parseable by the supplied parser (typically Nomad's /v1/jobs/parse), and must contain exactly one task with driver = "docker" or "podman" for the result to be wizardable. Anything more complex (multi-task, other drivers, multiple groups) returns wizardable=false so the UI disables the wizard.

volumes is the Volume Definition (YAML) text from the template; it is parsed into VolumeDefinitions independently of the HCL.

func RefreshNow

func RefreshNow(url string) (bool, error)

RefreshNow forces an immediate fetch from url (the API / admin-CLI path) and stores the result as the fetched overlay iff it is newer than the baseline. Returns whether the fetched manifest became the active catalog. Callers use FetchDecision to decide whether to refresh at all and from which URL.

func Reload

func Reload()

Reload is retained for compatibility; it clears the embedded parse cache so the next LoadManifest re-parses. (External files are never cached.)

func ResetFetched

func ResetFetched()

ResetFetched clears any in-memory fetched manifest so the embedded default is used again. Mainly for tests.

func SetFetchedManifest

func SetFetchedManifest(data []byte, source string) (bool, error)

SetFetchedManifest parses a downloaded manifest body and stores it as the fetched overlay iff its manifest_version is strictly newer than both the current baseline (external file if configured, else embedded) and any previously stored fetch. Returns (true, nil) when stored, (false, nil) when ignored as not newer.

With a file configured, this is exactly "check if the remote is newer than the local file and use the remote; otherwise keep the local". The file is re-read here so a freshly-bumped file version is respected.

Types

type HCLParser

type HCLParser func(hcl string) (map[string]interface{}, error)

HCLParser parses a Nomad HCL job string into the JSON shape returned by Nomad's /v1/jobs/parse endpoint. Implementations typically wrap a Nomad client; tests can supply a fake. A nil parser causes ParseNomadHCL to report the spec as not wizardable.

type ImageEntry

type ImageEntry struct {
	Name          string   `toml:"name" json:"name" msgpack:"name"`
	DisplayName   string   `toml:"display_name" json:"display_name" msgpack:"display_name"`
	Description   string   `toml:"description" json:"description" msgpack:"description"`
	Image         string   `toml:"image" json:"image" msgpack:"image"`
	Icon          string   `toml:"icon" json:"icon" msgpack:"icon"`
	Category      string   `toml:"category" json:"category" msgpack:"category"`
	Tags          []string `toml:"tags" json:"tags" msgpack:"tags"`
	DefaultMemory string   `toml:"default_memory" json:"default_memory" msgpack:"default_memory"`
	DefaultCPUs   string   `toml:"default_cpus" json:"default_cpus" msgpack:"default_cpus"`
	DefaultCores  string   `toml:"default_cores" json:"default_cores" msgpack:"default_cores"`
	Recommended   bool     `toml:"recommended" json:"recommended" msgpack:"recommended"`

	// DefaultEnv are KEY=value env vars pre-filled when picking this image
	// (e.g. KNOT_VNC_HTTP_PORT=5680 for desktop images).
	DefaultEnv []string `toml:"default_env" json:"default_env,omitempty" msgpack:"default_env,omitempty"`

	// DefaultPorts are template-level ports pre-filled when picking this
	// image (e.g. Web:80:http for PHP images). These are the template port
	// metadata, not the Nomad/Docker network ports.
	DefaultPorts []ManifestPort `toml:"default_port" json:"default_port,omitempty" msgpack:"default_port,omitempty"`

	// Volumes are mount points the image expects to be backed by persistent
	// storage (e.g. /home for Ubuntu/PHP, /data for Valkey/Redis,
	// /var/lib/mysql for MariaDB). The wizard pre-fills one storage row per
	// entry when the user picks this image. Only the mount point is declared
	// here; the backing kind (named volume / managed path / bind) is chosen in
	// the wizard.
	Volumes []ManifestVolume `toml:"volume" json:"volumes,omitempty" msgpack:"volumes,omitempty"`
}

ImageEntry describes a single base image in the wizard catalog. The Image field may contain template variables (e.g. ${{ .server.base_image_registry }}); those are resolved at render time by the wizard caller, not by the loader.

type Manifest

type Manifest struct {
	// Version is the manifest schema version (currently 1). Bumped only when
	// the structure of this file changes in a backward-incompatible way.
	Version int `toml:"version" json:"version" msgpack:"version"`

	// ManifestVersion is the catalog revision, used to decide whether a
	// fetched manifest is newer than the built-in one. Format yyyymmddbb
	// (date + zero-padded same-day build counter); lexicographic compare
	// equals chronological compare. Empty means "unset / oldest".
	ManifestVersion string `toml:"manifest_version" json:"manifest_version,omitempty" msgpack:"manifest_version,omitempty"`

	Description string       `toml:"description" json:"description" msgpack:"description"`
	Images      []ImageEntry `toml:"image" json:"images" msgpack:"images"`

	// RegistryAuth is set by the API handler (not parsed from TOML) to tell
	// the wizard whether server.base_image.registry_user/password are
	// configured, so it can inject an auth block when picking an image.
	RegistryAuth bool `toml:"-" json:"registry_auth" msgpack:"-"`
}

Manifest is the parsed catalog of base images plus catalog metadata.

func LoadManifest

func LoadManifest(cfg *config.ServerConfig) (*Manifest, error)

LoadManifest returns the active manifest. The model is "newest wins" between a baseline and an optional fetched overlay:

  1. Baseline = the external file (cfg.BaseImagesManifest), re-read on every call so admins can update the file in place; otherwise the embedded default.
  2. If a manifest fetched from the update URL is in memory AND its manifest_version is strictly newer than the baseline, it overlays the baseline. A fetched copy older than or equal to the baseline is ignored.

So with a file configured, the file is served as-is unless auto-update has fetched a newer remote copy — in which case the remote wins. If the admin bumps the file's version above the fetched one, the file wins again on the next request (the file is re-read every call).

type ManifestPort

type ManifestPort struct {
	Name     string `toml:"name" json:"name" msgpack:"name"`
	Port     uint16 `toml:"port" json:"port" msgpack:"port"`
	Protocol string `toml:"protocol" json:"protocol" msgpack:"protocol"`
}

ManifestPort is a template-level port entry in the manifest.

type ManifestVolume

type ManifestVolume struct {
	Path string `toml:"path" json:"path" msgpack:"path"`
	// Name is the suggested name for the wizard's storage row when the kind is
	// a managed volume (it is ignored for "bind"/"path" kinds). It may carry
	// deploy-time template variables — e.g. "${{ .space.id }}-home" — so that
	// the resolved volume name is unique per space and won't clash with other
	// spaces on the same host. Empty falls back to a name derived from Path
	// (the last path segment), which is NOT unique per space and may clash.
	Name string `toml:"name" json:"name,omitempty" msgpack:"name,omitempty"`
	// Kind is the suggested backing kind for the wizard's storage row:
	// "bind", "volume", or "path". Empty (the default) means "volume". Invalid
	// values are normalised to empty by the parser. The user can still change
	// the kind in the wizard after picking the image.
	Kind        string `toml:"kind" json:"kind,omitempty" msgpack:"kind,omitempty"`
	Description string `toml:"description" json:"description,omitempty" msgpack:"description,omitempty"`
}

ManifestVolume is a mount point within the container that the image expects to be persistent. Path is the in-container mount point (e.g. /data).

Jump to

Keyboard shortcuts

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