oac

package module
v0.0.0-...-defa035 Latest Latest
Warning

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

Go to latest
Published: May 21, 2026 License: AGPL-3.0 Imports: 20 Imported by: 0

README

oac API & OlaresManifest Validation Rules

oac lints an Olares App (aka oac) Helm chart directory: it validates OlaresManifest.yaml, dry-runs the chart, collects the image list, and checks container resource limits and ServiceAccount RBAC permissions.

This document covers two things:

  1. Public API: how external projects should call into the library
  2. OlaresManifest.yaml validation rules: what is currently being checked

Public API

All exported symbols live in a single public package:

Package Role
github.com/beclab/Olares/framework/oac Main entry point: build a Checker, run lint/validate, load config, list images; AppConfiguration is re-exported as an alias

Sub-packages under internal/ are not public — external callers should not (and cannot) import them directly.

Schema types come straight from upstream: oac.AppConfiguration and all of its nested structs are type aliases from github.com/beclab/api/manifest (Entrance / ServicePort / TailScale / ACL come from github.com/beclab/api/api/app.bytetrade.io/v1alpha1, AppEnvVar from github.com/beclab/api/api/sys.bytetrade.io/v1alpha1, and LabelSelector / LabelSelectorRequirement from k8s.io/apimachinery/.../meta/v1). *oac.AppConfiguration and *apimanifest.AppConfiguration share the same reflect.Type — when you need to build a manifest.Chart{...} / appv1.Entrance{...} value, import the upstream package directly; the oac package no longer re-exports them.

The Manifest interface ("the smallest common surface for a parsed manifest"):

type Manifest interface {
    APIVersion() string        // "v1" / "v2" / "v3"
    ConfigVersion() string     // olaresManifest.version
    ConfigType() string        // olaresManifest.type, typically "app"
    AppName() string           // metadata.name
    AppVersion() string        // metadata.version
    Entrances() []EntranceInfo
    OptionsImages() []string   // options.images
    PermissionAppData() bool
    Raw() any                  // type-assert: m.Raw().(*oac.AppConfiguration)
                                // or just use oac.AsAppConfiguration(m)
                                // underlying type: *github.com/beclab/api/manifest.AppConfiguration
}

When you need the full AppConfiguration struct to access detailed fields, don't reach for Raw() — use the typed API in §4 below.

2. Constructor
// Build a reusable Checker
c := oac.New(opts...)

// Load without validating
m, err  := c.LoadManifestFile(path)
m, err  := c.LoadManifestContent(bytes)

// Validate the manifest only (no helm, no folder check)
err := c.ValidateManifestFile(path)
err := c.ValidateManifestContent(bytes)
err := c.ValidateAppConfiguration(cfg)              // *AppConfiguration already in memory

// Full lint: folder layout + manifest + resources + optional checks
err := c.Lint(path)

// Individual checks
err := c.CheckChartFolder(path)                    // folder layout
err := c.CheckResources(path)                      // helm dry-run + container limits (§3.1); upload mount / workload naming are only enforced by Lint
err := c.CheckServiceAccountRules(path)            // RBAC forbidden rules
err := c.CheckSameVersion(path, m /* may be nil */) // Chart.yaml ↔ manifest version match

// List every image (helm-rendered workload images ∪ options.images, deduped and sorted)
imgs, err := c.ListImages(path)
// Same, but render the chart with .Values.GPU.Type = mode so GPU-gated
// templates contribute their workloads (empty mode == ListImages).
imgs, err := c.ListImagesForMode(path, "nvidia")
// Multi-mode: union of images across each mode, dedup'd. Pass nil/empty for
// default-branch render; pass "all" to expand into oac.AllImageRenderModes.
imgs, err := c.ListImagesForModes(path, []string{"nvidia", "cpu"})
imgs, err := c.ListImagesForModes(path, []string{"all"})

Every helm dry-run (Lint, ListImages / ListImagesForMode, CheckResources, CheckServiceAccountRules, ...) deep-merges any values registered via oac.WithValues(map[string]interface{}{...}) on top of the scaffold built by helmrender.BuildValues. External keys win on conflicts; map keys recurse so siblings the caller did not override survive. The per-mode .Values.GPU.Type set by ListImagesForMode and the resource-limit per-mode loop runs after the merge, so it still wins on GPU.Type.

3. Top-level convenience functions

If you don't want to hold on to a Checker, use these shortcuts (each is just New(opts...).Xxx(...)):

err  := oac.Lint(path, opts...)
err  := oac.ValidateManifestFile(path, opts...)
err  := oac.ValidateManifestContent(bytes, opts...)
imgs, err := oac.ListImagesFromOAC(path, opts...)
imgs, err := oac.ListImagesFromOACForMode(path, "nvidia", opts...)
imgs, err := oac.ListImagesFromOACForModes(path, []string{"nvidia", "cpu"}, opts...)
imgs, err := oac.ListImagesFromOACForModes(path, []string{"all"}, opts...) // expands to oac.AllImageRenderModes

// Parse straight into a typed *AppConfiguration (no validation)
cfg, err  := oac.LoadAppConfiguration(path, opts...)
cfg, err  := oac.LoadAppConfigurationContent(bytes, opts...)

// Validate an already-built *AppConfiguration (same rules as ValidateManifestFile)
err := oac.ValidateAppConfiguration(cfg, opts...)

// Run both admin==owner and admin!=owner scenarios (Lint once for each)
err := oac.LintBothOwnerScenarios(path, extraOpts...)

// Peek the two top-level version fields without a full parse
v, err := oac.PeekManifestVersions(bytes)
// v.APIVersion             -> apiVersion            ("v1" / "v2" / "v3" / ...)
// v.OlaresManifestVersion  -> olaresManifest.version ("0.12.0" / ...)

// Resolve the effective resource envelope for a single spec.resources[] mode
// (see §7 below)
lim, err := oac.ResourceLimitsForResourceMode(cfg, "nvidia")
4. Typed access to AppConfiguration

*Manifest.Raw() returns any; you need a type assertion to get at the fields. Most callers just want "one line to get a typed cfg", so the root package provides a thin wrapper:

// Alias: oac.AppConfiguration == github.com/beclab/api/manifest.AppConfiguration
// (same underlying type, same reflect.Type)
type AppConfiguration = apimanifest.AppConfiguration

// Checker methods (parse only, no validation)
func (c *Checker) LoadAppConfiguration(oacPath string) (*AppConfiguration, error)
func (c *Checker) LoadAppConfigurationContent(content []byte) (*AppConfiguration, error)

// Top-level convenience functions
func LoadAppConfiguration(oacPath string, opts ...Option) (*AppConfiguration, error)
func LoadAppConfigurationContent(content []byte, opts ...Option) (*AppConfiguration, error)

// Validate a parsed / hand-built *AppConfiguration with the exact same rules as
// ValidateManifestFile (field-level + cross-field; no helm / folder / resource-level
// checks and no customValidators — those need an oacPath and chart templates, which
// a bare *AppConfiguration can't provide).
//
// Behavior:
//   - Honors SkipManifestCheck(): returns nil immediately when enabled.
//   - Does not panic on cfg == nil; returns a *ValidationError the caller can
//     unwrap with errors.As.
//   - Failures are wrapped in *ValidationError; Version is taken from
//     cfg.APIVersion (defaults to "v1" when empty).
func (c *Checker) ValidateAppConfiguration(cfg *AppConfiguration) error

// Top-level shortcut (equivalent to New(opts...).ValidateAppConfiguration(cfg))
func ValidateAppConfiguration(cfg *AppConfiguration, opts ...Option) error

// Extract a *AppConfiguration from an existing Manifest (nil-safe; today's
// Strategy always returns true).
func AsAppConfiguration(m Manifest) (*AppConfiguration, bool)

If you only need to read fields, oac.AppConfiguration is enough:

cfg, _ := oac.LoadAppConfiguration("./my-app")
fmt.Println(cfg.Metadata.Name, cfg.Spec.Resources[0].Mode)
for _, e := range cfg.Entrances { fmt.Println(e.Name, e.Port) }

When you need to construct nested structs (generating a manifest, writing test fixtures, patching a field), import the upstream type packages directly — the oac package does not forward them:

import (
    oac "github.com/beclab/Olares/framework/oac"

    appv1 "github.com/beclab/api/api/app.bytetrade.io/v1alpha1"
    "github.com/beclab/api/manifest"
)

cfg := &oac.AppConfiguration{ // equivalent to *manifest.AppConfiguration
    APIVersion: manifest.APIVersionV1, // or just the literal "v1" if not exported upstream
    Metadata:   manifest.AppMetaData{Name: "demo", Title: "Demo", Version: "1.0.0"},
    Entrances:  []appv1.Entrance{{Name: "web", Host: "demo", Port: 8080}},
    Spec: manifest.AppSpec{
        SubCharts: []manifest.Chart{{Name: "extras", Shared: true}},
        Resources: []manifest.ResourceMode{{
            Mode: "nvidia",
            ResourceRequirement: manifest.ResourceRequirement{
                RequiredCPU: "200m", LimitedCPU: "1",
                RequiredGPU: "1",   LimitedGPU: "1",
            },
        }},
    },
    Options: manifest.Options{Upload: manifest.Upload{Dest: "/data", LimitedSize: 1024}},
}

Schema type layout

Source package Types it covers
github.com/beclab/api/manifest The vast majority: AppConfiguration, AppMetaData, AppSpec, Chart, Provider, Permission, ProviderPermission, Policy, Dependency, Conflict, Options, ResetCookie, AppScope, WsConfig, Upload, OIDC, the Middleware family (Database, PostgresConfig, ArgoConfig, MinioConfig, Bucket, RabbitMQConfig, VHost, ElasticsearchConfig, Index, RedisConfig, MongodbConfig, MariaDBConfig, MySQLConfig, ClickHouseConfig, NatsConfig, Subject, Export, Ref, RefSubject, PermissionNats), Hardware / CpuConfig / GpuConfig / SupportClient, ConfigOverlay, ResourceRequirement / ResourceMode / SpecialResource
github.com/beclab/api/api/app.bytetrade.io/v1alpha1 (appv1) Entrance, ServicePort, TailScale, ACL
github.com/beclab/api/api/sys.bytetrade.io/v1alpha1 (sysv1alpha1) AppEnvVar
k8s.io/apimachinery/pkg/apis/meta/v1 (metav1) LabelSelector, LabelSelectorRequirement

Note: LoadAppConfiguration* does not validate — it only runs the parse pipeline (legacy goes through template rendering, modern is parsed literally). If you need "parse + validate + get the config", call LoadAppConfiguration first, then ValidateAppConfiguration(cfg) (or ValidateManifestContent(bytes)).

About the old (*AppConfiguration).Validate() method: it has been removed — AppConfiguration is now a type alias of github.com/beclab/api/manifest.AppConfiguration, and Go does not allow methods on aliases. Use the (*Checker).ValidateAppConfiguration(cfg) method or the package-level oac.ValidateAppConfiguration(cfg, opts...) instead; the rules are identical.

How options affect ValidateAppConfiguration: currently only SkipManifestCheck() affects this entry point (returns nil immediately when on). Other options (owner/admin, SkipResourceCheck, SkipAppDataCheck / WithAppDataValidator, ...) need helm rendering, the chart directory, or chart templates — none of which a bare *AppConfiguration can provide — so passing them is a silent no-op.

Migrating from appcfg: the same types were once also re-exported through the github.com/beclab/Olares/framework/oac/appcfg sub-package, which has been removed. Callers should import the three upstream packages listed above directly; the aliases match, so the types are fully compatible — just swap the import and prefix (appcfg.Chartmanifest.Chart, appcfg.Entranceappv1.Entrance, appcfg.LabelSelectormetav1.LabelSelector).


5. Options
Option Default Description
WithOwner(name) empty Sets .Values.bfl.username in the chart template; empty values are ignored
WithAdmin(name) empty Sets .Values.admin in the chart template; empty values are ignored
WithOwnerAdmin(name) owner and admin take the same value ("admin self-install" scenario)
WithAutoOwnerScenarios() off Lint ignores any explicit WithOwner* above and automatically renders + checks workloads for both owner==admin and owner!=admin installation scenarios; both must pass. Manifest validation, folder checks, and same-version checks run only once (they don't depend on owner). Equivalent to LintBothOwnerScenarios
WithoutAutoOwnerScenarios() Clears autoOwner and falls back to a single run using the explicit owner/admin values. Useful for turning the auto mode off at a specific call site when composing option sets
SkipFolderCheck() off Skip the folder-layout check
SkipManifestCheck() off Skip manifest structural validation
SkipResourceCheck() off Skip container resource limit checks. Note: upload mount point and workload naming are structural integrity checks and are always enforced by Lint, regardless of this option
SkipSameVersionCheck() on Disable the Chart.yaml ↔ manifest version match check. It is on by default; turn it off when you want to align version numbers separately before publishing to the app store
WithSameVersionCheck() Turn the match check back on (useful when composing option sets to re-enable it at a specific call site)
WithServiceAccountRulesCheck() off Enable the ServiceAccount RBAC forbidden-rules check
WithSecurityContextCheck() off Enable the non-beclab image privileged securityContext check. The check rejects any container (init or main) whose image is NOT under the beclab/ namespace (matched as beclab/... prefix or as a /beclab/... path segment after a registry hostname) and whose effective securityContext sets any of privileged: true, runAsUser: 0, runAsNonRoot: false. Pod-level securityContext is considered: if the pod sets runAsUser: 0 or runAsNonRoot: false, every non-beclab container inherits the unsafe default and is reported. Walks Deployment / StatefulSet / DaemonSet workloads
SkipSecurityContextCheck() Clears the non-beclab securityContext check flag. The check is OFF by default; this option only matters when composing on top of an option set that previously turned it on
WithCustomValidator(fn) Register a custom CustomValidator; can be called multiple times to append
SkipAppDataCheck() on Disable the built-in .Values.userspace.appdata cross-check. The check is on by default; turn it off only when a chart legitimately renders appdata via a non-standard path
WithAppDataValidator() Re-enable the built-in .Values.userspace.appdata cross-check. Kept as a back-compat alias — the check now runs by default and is disabled with SkipAppDataCheck(). Calling this on a fresh checker is a no-op
SkipHostPathCheck() on Disable the built-in hostPath + rolling-update incompatibility check. The check rejects any Deployment (strategy = RollingUpdate / unset) or StatefulSet (updateStrategy = RollingUpdate / unset) that mounts a hostPath volume, because a rolling update can land the new pod on a different node where the host directory does not exist. Allowed strategies: Recreate (Deployment) and OnDelete (StatefulSet)
WithHostPathCheck() Re-enable the built-in hostPath + rolling-update check after a previous option set disabled it. No-op on a fresh checker
SkipResourceNamespaceCheck() on Disable the rendered-resource namespace check. The check enforces: workloads (Deployment / StatefulSet / DaemonSet) must have metadata.namespace = "app-namespace"; other namespaced resources must be in "app-namespace" or a "user-system-*" namespace; cluster-scoped resources (empty namespace) are skipped
WithResourceNamespaceCheck() Re-enable the rendered-resource namespace check after a previous option set disabled it. No-op on a fresh checker

Note: New()'s default behavior is "run everything except ServiceAccountRules" — the Chart.yaml ↔ manifest version match check, the .Values.userspace.appdata cross-check, the hostPath + rolling-update check, and the rendered-resource namespace check all run by default. Use Skip* to turn off a built-in check, and With*Check to turn on one that is off by default.

6. Typical usage

Run a full lint in one line:

import oac "github.com/beclab/Olares/framework/oac"

if err := oac.Lint("./my-app",
    oac.WithOwnerAdmin("root"),
    // same-version check is on by default; use SkipSameVersionCheck()
    // here if you don't want Chart.yaml ↔ manifest alignment enforced.
); err != nil {
    log.Fatal(err)
}

Reuse a Checker across multiple charts:

c := oac.New(
    oac.WithOwner("alice"),
    oac.WithAdmin("root"),
    oac.WithServiceAccountRulesCheck(),
    // .Values.userspace.appdata cross-check runs by default; pass
    // oac.SkipAppDataCheck() here only if you need to bypass it.
)
for _, p := range paths {
    if err := c.Lint(p); err != nil {
        log.Printf("%s: %v", p, err)
    }
}

Just the image list:

// Default branch only.
images, err := oac.ListImagesFromOAC("./my-app", oac.WithOwnerAdmin("root"))

// Union across a few specific GPU.Type modes (deduped + sorted).
images, err := oac.ListImagesFromOACForModes(
    "./my-app",
    []string{"nvidia", "cpu"},
    oac.WithOwnerAdmin("root"),
)

// Union across *every* mode advertised by AllImageRenderModes
// (currently cpu / apple-m / nvidia / nvidia-gb10 / mthreads-m1000 /
// strix-halo). Mixing "all" with explicit modes is fine — duplicates
// collapse to a single render per mode.
images, err := oac.ListImagesFromOACForModes(
    "./my-app",
    []string{"all"},
    oac.WithOwnerAdmin("root"),
)

Validate manifest content only (no chart directory needed):

if err := oac.ValidateManifestContent(yamlBytes); err != nil {
    var ve *oac.ValidationError
    if errors.As(err, &ve) {
        fmt.Printf("apiVersion=%s, field=%s, reason=%s\n", ve.Version, ve.Field, ve.Reason)
    }
}

Pass both admin-install and user-install scenarios (two equivalent forms):

err := oac.LintBothOwnerScenarios("./my-app")
// or via the option directly:
err = oac.Lint("./my-app", oac.WithAutoOwnerScenarios())

Typed read of the config only (no validation):

cfg, err := oac.LoadAppConfiguration("./my-app")
if err != nil { log.Fatal(err) }
for _, r := range cfg.Spec.Resources {
    fmt.Println(r.Mode, r.RequiredCPU, r.LimitedCPU)
}

Switch from an existing Manifest to a typed view:

m, _ := oac.New().LoadManifestFile("./my-app")
if cfg, ok := oac.AsAppConfiguration(m); ok {
    _ = cfg.Options.Upload.Dest
}

Hand-built / programmatically generated cfg, then validate:

cfg := &oac.AppConfiguration{ /* ... */ } // equivalent to *manifest.AppConfiguration

// Top-level convenience for a one-shot validation
if err := oac.ValidateAppConfiguration(cfg); err != nil {
    var ve *oac.ValidationError
    if errors.As(err, &ve) { /* ... */ }
}

// Method form when reusing a Checker alongside other entry points (Lint / LoadAppConfiguration)
c := oac.New()
if err := c.ValidateAppConfiguration(cfg); err != nil { /* ... */ }
7. Lightweight helpers

These helpers cover lookups that don't need a full lint or helm render.

7.1 PeekManifestVersions

Extracts apiVersion and olaresManifest.version from raw YAML using the same line-oriented regex probe as the version-dispatch pipeline (see §9 — tolerant of unrendered Helm template blocks, quoted / commented values, and CRLF). Useful when you only want to decide which pipeline or schema applies, without paying for parse + validate.

// Versions is a trivial DTO; field names match the YAML keys (apiVersion /
// olaresManifest.version).
type ManifestVersions struct {
    APIVersion            string
    OlaresManifestVersion string
}

func PeekManifestVersions(content []byte) (ManifestVersions, error)
  • Missing keys collapse to empty strings rather than raising an error.
  • Only low-level scanner failures (e.g. an unreadable buffer) produce a non-nil error.
  • Lines whose first byte is ' ', '\t', '-', or '#' are ignored, so nested / commented re-definitions don't poison the result.
v, err := oac.PeekManifestVersions(yaml)
if err != nil { /* ... */ }
switch {
case v.APIVersion == "v2":
    // v2-specific path
case v.OlaresManifestVersion == "":
    // treat as legacy
}
7.2 ResourceLimitsForResourceMode

Resolves the CPU / memory / disk / GPU envelope that applies to one spec.resources[] row. Returns the inline ResourceRequirement declared on the matched mode, exposed as a pure function so that external tooling can compute "what should the limits be for this mode" without invoking helm.

// Same eight-field shape as ResourceRequirement: cpu / memory / disk / gpu,
// each with a required and a limited entry. Fields not declared on the
// manifest collapse to "".
type ManifestResourceLimits = manifest.ResourceRequirementLimits

func ResourceLimitsForResourceMode(
    cfg *AppConfiguration,
    mode string, // e.g. "nvidia", "amd-apu" — matched case-insensitively
) (ManifestResourceLimits, error)

Errors:

  • cfg == nil"oac: AppConfiguration is nil".
  • No spec.resources[] entry whose mode matches → "oac: no spec.resources entry with mode ...".
lim, err := oac.ResourceLimitsForResourceMode(cfg, "nvidia")
if err != nil { /* ... */ }
fmt.Println(lim.RequiredCPU, lim.LimitedCPU, lim.RequiredGPU, lim.LimitedGPU)

8. Error model
  • Validation errors are returned as *oac.ValidationError (unwrap with errors.As to get the fields)
  • AggregateErrors([]error) merges multiple errors into one (nil / empty slice returns nil)
  • All non-validation errors (IO, helm render, RBAC parsing, etc.) are returned as plain error
  • For legacy manifests (olaresManifest.version < 0.12.0), validation runs parse+validate twice — once with admin==owner template rendering and once with admin!=owner — and aggregates failures into a single ValidationError
9. Version dispatch pipeline

Callers don't need to worry about this, but it's worth knowing:

  • Probe: manifest.Peek uses regex to pull olaresManifest.version / apiVersion from the raw YAML (tolerates unrendered {{ ... }})
  • Dispatch: manifest.NewPipeline(olaresVersion, defaultStrategy)
    • < 0.12.0dualOwnerPipeline: must render the chart through helm; validation runs parse+validate in both admin==owner and admin!=owner scenarios
    • >= 0.12.0 / empty / malformed → singlePipeline: parses the YAML literally, no template rendering
  • Strategy instance: the root package only wires a single &manifest.ManifestStrategy{} (stateless, goroutine-safe), responsible for parse + validate of v1 / v2 / v3 apiVersion
  • oac.ManifestFileName exports the "OlaresManifest.yaml" constant

OlaresManifest.yaml validation rules

Validation is organized in three layers:

  1. Structural rules (internal/manifest.ValidateAppConfiguration, exposed by the root package as oac.ValidateAppConfiguration(cfg, opts...) and (*Checker).ValidateAppConfiguration(cfg)): field-level validation driven by ozzo-validation, including spec rules that depend on olaresManifest.version (legacy flat quantities required < 0.12.0 vs modern spec.resources[] required >= 0.12.0) plus version-independent cross-field rules (Rule 7 mutual exclusion, mode → supportArch, completeness)
  2. Cross-field rules (checkSubCharts only): v2 spec.subCharts layout rules that require looking at apiVersion and spec
  3. Resource-level rules (the resources sub-package): after a helm dry-run, additional checks on the generated Kubernetes resources

Lint also runs a folder-layout check (chartfolder) up front — see §3.5.


1. Structural rules (field-level)
1.1 Top-level AppConfiguration
Field Rule
olaresManifest.version required
apiVersion Must be "v1", "v2", or "v3" when non-empty; empty defaults to v1
metadata Recursive validation (see §1.2)
entrances required, length 1..10, name must be unique (uniqueEntranceNames)
spec required, recursive validation (see §1.3)
permission Recursive (no additional rules at the permission level today)
options Recursive validation (see §1.4)
1.2 metadata (AppMetaData)
Field Rule
name required, 1–30 characters
icon required
description required
title required, 1–30 characters
version required, must be a valid SemVer (e.g. 1.2.3)
1.3 spec (AppSpec)

Validation pivots on olaresManifest.version (ConfigVersion):

olaresManifest.version Flat spec.required* / spec.limited* (cpu/memory/disk) spec.resources[]
< 0.12.0 requiredCpu / limitedCpu / requiredMemory / limitedMemory / requiredDisk are required and quantity-checked; limitedDisk is optional and quantity-checked Not enforced as required. Per-element rules in §2.2 still run on whatever entries are present
≥ 0.12.0 Not enforced at the field level — these fields must instead be empty by virtue of the mutual-exclusion rule below (Rule 7) when spec.resources[] is set spec.resources is required (must be non-empty). Each entry follows §2.2

spec.requiredGpu and spec.limitedGpu are always optional and quantity-checked when set, regardless of version.

In addition, regardless of version, Rule 7 (mutual exclusion) applies: when spec.resources[] is non-empty, none of the eight legacy flat fields (spec.requiredCpu / spec.limitedCpu / spec.requiredMemory / spec.limitedMemory / spec.requiredDisk / spec.limitedDisk / spec.requiredGpu / spec.limitedGpu) may be set. The two are alternative shapes of the same envelope — pick one.

Field Rule
requiredMemory / requiredDisk / requiredCpu / limitedMemory / limitedCpu Required + quantity regex when < 0.12.0; must be empty when >= 0.12.0 and spec.resources[] is set (Rule 7)
limitedDisk Optional + quantity regex when < 0.12.0; must be empty when >= 0.12.0 and spec.resources[] is set (Rule 7)
requiredGpu / limitedGpu Always optional + quantity regex when non-empty; must be empty when spec.resources[] is set (Rule 7)
resources[] Required when >= 0.12.0. Recursive: each ResourceMode follows the rules in §2.2

The Kubernetes Quantity regex covers common forms: 100m, 1.5, 512Mi, 2Gi, 1e9, etc.

Empty-spec optimisation: instead of cascading every per-field "is required" rule when the user supplies nothing, validateAppSpec collapses the missing-everything case into a single, version-tagged guidance message:

  • < 0.12.0 (legacy): when all five required legacy fields (requiredCpu, limitedCpu, requiredMemory, limitedMemory, requiredDisk) are empty, the per-field cascade is suppressed and the validator emits one consolidated message: spec.requiredCpu / spec.limitedCpu / spec.requiredMemory / spec.limitedMemory / spec.requiredDisk are required for olaresManifest.version < 0.12.0; populate the legacy resource envelope. Partial fills (one or more legacy fields set) still produce pinpointed per-field "is required" errors.
  • ≥ 0.12.0 (modern): when spec.resources is empty, the validator emits one message: spec.resources is required for olaresManifest.version >= 0.12.0; declare at least one entry.

The cross-field rules in §2.2 still run as appropriate for the version: Rule 7 (mutual exclusion) always fires when spec.resources[] is non-empty, while Rule 1 (mode → supportArch) and Rule 4-empty (entry completeness) are scoped to >= 0.12.0. The optimisation only suppresses the noisy per-field "Required" cascade when there is nothing to attribute it to.

1.4 Each entrances[i] (Entrance)
Field Rule
name Matches ^[a-z0-9A-Z-]*$, length ≤ 63
host Matches ^[a-z]([-a-z0-9]*[a-z0-9])$, length ≤ 63
port Must be > 0 (ozzo's Min skips unset zero values, so 0 still passes, but negatives like -1 are rejected)
icon When non-empty, must be a valid http:// / https:// URL
title required, 1–30 characters, matches ^[a-z0-9A-Z-\s]*$
authLevel One of "", "public", "private"
openMethod One of "", "default", "iframe", "window"

Additionally, all entrances' name fields must be unique (enforced by the top-level uniqueEntranceNames rule).

1.5 options
Sub-field Rule
policies[] Recursive: each Policy requires uriRegex and level; validDuration must match ^((?:[-+]?\d+(?:\.\d+)?([smhdwy]|us|ns|ms))+)$ when non-empty
resetCookie Placeholder (no rules)
dependencies[] Each Dependency requires name and version; type must be "system" or "application"
appScope Placeholder
websocket Placeholder

2. Cross-field rules
2.1 checkSubCharts (unconditionally triggered by apiVersion=v2)
  • spec.subCharts must exist and be non-empty
  • At least one entry in spec.subCharts[] must have shared: true

This rule only looks at apiVersion and is independent of olaresManifest.version. Even if an older chart is on olaresManifest.version: 0.11.0, as long as it declares apiVersion: v2, checkSubCharts still fires — don't confuse it with the olaresManifest.version threshold in §1.3 for flat fields vs spec.resources[].

2.2 spec.resources[] per-element and modern-only cross-field rules (see §1.3)

Each spec.resources[i] is a ResourceMode:

- mode: nvidia
  # inline ResourceRequirement (required/limited for cpu/memory/disk/gpu)
  requiredCpu: 100m
  limitedCpu: 200m
  requiredMemory: 128Mi
  limitedMemory: 256Mi
  requiredDisk: 1Gi
  limitedDisk: 2Gi
  requiredGpu: 8Gi
  limitedGpu: 8Gi

Valid mode values: cpu, amd-apu, amd-gpu, apple-m, nvidia, nvidia-gb10, mthreads-m1000.

Per-element base rules (ValidateResourceMode, dispatched onto each ResourceMode via validation.Each):

  • mode is required and must be within the enum above
  • Every quantity field (requiredCpulimitedGpu) in the inline ResourceRequirement must match the k8s Quantity regex when non-empty

Cross-field rules (numbers match the code comments):

# Name Description
Rule 1 mode → supportArch GPU chip families imply a CPU architecture:
amd-gpu, nvidia → must include amd64
nvidia-gb10, mthreads-m1000 → must include arm64
Rule 3 Non-GPU modes must not declare GPU cpu / amd-apu / apple-m / nvidia-gb10 / mthreads-m1000 must leave requiredGpu / limitedGpu empty (via ensureNoGPUSection). requiredDisk / limitedDisk are allowed on all modes.
Rule 4 Section-completeness envelope (ensureSectionComplete) If the inline ResourceRequirement has at least one non-empty quantity field (hasAnyQuantity), it must be filled out completely:
requiredCpu, limitedCpu, requiredMemory, limitedMemory, requiredDisk, limitedDisk — all six required
• If mode{nvidia, amd-gpu}, requiredGpu and limitedGpu are additionally required as a complete pair
Rule 4-empty Empty entry rejected (requireResourceEntryFields) If the inline ResourceRequirement has no quantity fields at all, every standard field is reported as missing so that an empty mode-only declaration cannot slip through unnoticed. GPU pairs are reported alongside cpu/memory/disk on nvidia / amd-gpu
Rule 5 limited >= required For each dimension (cpu/memory/disk/gpu), when both required and limited are declared, limited must be ≥ required
Rule 7 Mutual exclusion of legacy flat fields (ensureLegacyAndResourcesAreMutuallyExclusive) When spec.resources[] is non-empty, the eight legacy flat fields spec.requiredCpu / spec.limitedCpu / spec.requiredMemory / spec.limitedMemory / spec.requiredDisk / spec.limitedDisk / spec.requiredGpu / spec.limitedGpu must all be empty. The two are alternative expressions of the same envelope and cannot coexist on a single manifest. Each violating field reports its own error and errors.Join aggregates them. Applies regardless of olaresManifest.version.

Versioning notes: specResourceCrossFieldRules is partially version-gated.

  • Rule 7 (mutual exclusion) runs at every olaresManifest.version. A legacy manifest that mixes spec.required* / spec.limited* with spec.resources[] is still rejected so users on the legacy schema cannot accidentally straddle both shapes.
  • Rule 1 (mode → supportArch) and Rule 4-empty (empty-entry completeness) only run when olaresManifest.version >= 0.12.0. spec.resources[] is not part of the legacy schema, so its inner contents are intentionally not inspected below the gate; legacy users are instead steered toward the flat fields by §1.3 (consolidated guidance when the legacy envelope is missing).
  • The version dispatch for "which shape is required" lives in validateAppSpec, not in specResourceCrossFieldRules. Per-element base rules (the mode enum, quantity validity, Rules 3 / 4 / 5) are fired by ozzo-validation via validation.Each on ValidateResourceMode; that wiring is also only added in the modern branch of validateAppSpec, so they never fire on legacy manifests either.

3. Resource-level rules (require a helm dry-run)

Checker.Lint first calls helmrender.BuildValues and helmrender.Render to produce the default kube.ResourceList. §3.2, §3.3 and optionally §3.4 run on that same list; when SkipResourceCheck() is not set, checkResourceLimits (§3.1) is invoked afterwards (modern manifests re-render additionally per mode, independent of the default list).

Checker.CheckResources also does one BuildValues + Render (except it returns immediately for apiVersion: v2, which skips container limit checks), then forwards the chart path and manifest to checkResourceLimits; for modern manifests the list produced by that render is not used by the limit branch (limit checks are entirely driven by per-mode re-renders), so it does not include §3.2 / §3.3.

For apiVersion: v2 the parent OAC root is not a renderable workload chart in the multi-chart install layout, so the helm dry-run for Lint / CheckServiceAccountRules / ListImages iterates spec.subCharts[] and concatenates the per-subchart kube.ResourceLists. apiVersion: v3 follows the same single-chart render path as v1 (whole chart at the OAC root). A non-empty apiVersion outside v1 / v2 / v3 fails validation and resource checks with not supported version.

Lint always runs the helm render. Upload mount and workload naming (§3.2, §3.3) are always enforced; container limits (§3.1) are gated by SkipResourceCheck(); RBAC (§3.4) is gated by WithServiceAccountRulesCheck().

3.1 Container limits (CheckResourceLimits, skippable)

For each Deployment and StatefulSet in the render, on the primary container:

  • Manifest side: requiredCpu <= limitedCpu, requiredMemory <= limitedMemory
  • Container side: every container must declare both requests and limits for CPU and memory
  • Container-side requests.cpu/memory <= limits.cpu/memory
  • Sum of all containers' requests.cpu/memory ≤ manifest-side requiredCpu/Memory
  • Sum of all containers' limits.cpu/memory ≤ manifest-side limitedCpu/Memory

Where the manifest-side limits come from (dispatched by olaresManifest.version and apiVersion):

  • apiVersion: v2 (any olaresManifest.version): the container limit check is skipped entirely. checkResourceLimits short-circuits before either the legacy or the modern branch, and CheckResources returns nil at its isV2Manifest guard without even rendering. Rationale: v2 is the multi-chart install layout — each spec.subCharts[] entry has its own quota, so summing container limits against the parent manifest's spec.required*/spec.limited* (or any single spec.resources[] row) is meaningless.
  • Legacy (< 0.12.0) with apiVersion unset, v1, or v3: read directly from the four flat fields spec.requiredCpu / spec.limitedCpu / spec.requiredMemory / spec.limitedMemory. Lint renders the chart once (sharing the render with §3.2 / §3.3) and compares that single kube.ResourceList against the four fields.
  • Modern (>= 0.12.0) with apiVersion unset, v1, or v3: validateAppSpec requires spec.resources[] and Rule 7 forbids mixing it with the flat fields, so limits come from spec.resources[]. For each ResourceMode rm:
    1. BuildValues(...) + SetGPUType(values, rm.Mode) (.Values.GPU.Type = <mode>).
    2. helmrender.Render(...) at oacPath produces the kube.ResourceList for that mode.
    3. Limits use the inline ResourceRequirement on that mode (requiredCpu, limitedCpu, requiredMemory, limitedMemory).
    4. CheckResourceLimits(list, limits). Failure prefix: resources mode=<mode>:.
  • Failures across modes are aggregated via errors.Join.
  • If spec.resources is empty (a modern manifest without any ResourceMode), this check is skipped entirely — such a chart has no limits to compare against, so there is nothing more to validate.
  • The upload-mount and workload-naming checks (§3.2 / §3.3) run only on the default render; per-mode re-renders serve only §3.1 and do not re-run §3.2 / §3.3.
3.2 Upload mount (CheckUploadConfig, always enforced)

If options.upload.dest is set in the manifest: the primary container of any rendered Deployment/StatefulSet must mount the same path in its volumeMounts (compared after filepath.Clean). Missing mounts are reported as an error.

3.3 Workload naming (CheckDeploymentName, always enforced)

When olaresManifest.type == "app": at least one Deployment or StatefulSet's rendered name must equal metadata.name. Non-app types skip this check.

During dry-run, helm's Release.Name is set to metadata.name (matching the "release name == app name" convention in production Olares), so templates using name: {{ .Release.Name }} pass this check as well.

3.4 ServiceAccount RBAC (CheckServiceAccountRules, off by default; enable with WithServiceAccountRulesCheck())

For every RoleBinding / ClusterRoleBinding rendered by the chart:

  • Find bindings where subject.kind == ServiceAccount and collect their roleRef.names
  • Pull the corresponding Role / ClusterRole rules
  • Compare them against the default forbidden set (DefaultForbiddenRules, overridable internally via LoadForbiddenRules("custom yaml")):
rules:
- apiGroups: ['*']
  resources:  [nodes, networkpolicies]
  verbs:      [create, update, patch, delete, deletecollection]

If any ServiceAccount binding grants any of those actions, the check fails.

3.5 Folder-layout check (CheckLayout, on by default)

The Lint entry point runs chartfolder.CheckLayout(path) first:

  • Directory name matches ^[a-z0-9]{1,30}$
  • Directory exists
  • Contains Chart.yaml, values.yaml, templates/, OlaresManifest.yaml

The further CheckConsistency (called by CheckSameVersion) also validates:

  • directory name == Chart.yaml's name == metadata.name
  • metadata.version == Chart.yaml's version

CheckWithTitle (used in the PR flow, not exposed at the root package) additionally requires:

  • Every entry in metadata.categories is within the fixed enum (AI / Blockchain / Utilities / Social Network / Data / Entertainment / Productivity / Lifestyle / Developer / Multimedia)
  • The directory name is not in the reserved list (user / system / space / default / os / kubesphere / kube / kubekey / kubernetes / gpu / tapr / bfl / bytetrade / project / pod)

4. Custom validators

Functions registered via WithCustomValidator(fn) are invoked after the built-in structural validation and before the resource-level checks:

type CustomValidator func(oacPath string, m Manifest) error

A built-in equivalent runs as a separate, always-on step (not via customValidators): if any file under the chart's templates/*.yaml references .Values.userspace.appdata, OlaresManifest.yaml must declare permission.appData: true, otherwise the check fails. It runs by default in Lint; pass SkipAppDataCheck() to disable it for a particular call. The legacy entry point WithAppDataValidator() is kept as a back-compat alias that simply re-asserts the on-by-default state.

Another always-on built-in (after the helm dry-run) is the hostPath + rolling-update incompatibility check: any Deployment whose spec.strategy.type is RollingUpdate (or empty, since that defaults to RollingUpdate on the API server side) and that mounts a hostPath volume — or any StatefulSet with spec.updateStrategy.type = RollingUpdate / empty doing the same — will fail Lint. The reason: a rolling update can land the new pod on a different node where the host directory does not exist, so the new pod silently starves on a volume that's "present" in the manifest but absent in practice. Use spec.strategy.type: Recreate on Deployments and updateStrategy.type: OnDelete on StatefulSets when you legitimately need a hostPath mount. Pass SkipHostPathCheck() to bypass the check; WithHostPathCheck() re-enables it after a previous disable.

A third always-on built-in (also after the helm dry-run) is the rendered-resource namespace check: every workload that comes out of the dry-run with metadata.namespace set must conform to the install-time contract. Specifically:

  • Deployment / StatefulSet / DaemonSet must declare metadata.namespace = "app-namespace" (the same value as helmrender.RenderNamespace, i.e. the namespace Olares uses to install every app's chart). Workloads in any other namespace would silently miss the app's quota / NetworkPolicy / sidecar injection.
  • Any other namespaced resource (Service, ConfigMap, Secret, Role, RoleBinding, ProviderRegistry, ...) must land in app-namespace or in a namespace whose name starts with user-system- (the convention for cross-namespace bridges into the owner's user-system namespace).
  • Cluster-scoped resources (ClusterRole, ClusterRoleBinding, PersistentVolume, ...) and any resource whose metadata.namespace is empty are skipped — they have no notion of namespace by design.

Use {{ .Release.Namespace }} for in-app resources (helm fills it with app-namespace) and an explicit user-system-{{ .Values.bfl.username }} for bridges into the owner's user-system namespace; both render to compliant values. Pass SkipResourceNamespaceCheck() to bypass the check; WithResourceNamespaceCheck() re-enables it after a previous disable.

An opt-in built-in is the non-beclab image privileged securityContext check: rejects any container (init or main) whose image lives outside the beclab/ namespace and whose effective securityContext grants root-equivalent privileges:

  • container.securityContext.privileged: true
  • container.securityContext.runAsUser: 0
  • container.securityContext.runAsNonRoot: false

Pod-level spec.securityContext is also examined; if it sets runAsUser: 0 or runAsNonRoot: false, every non-beclab container that does not override those fields is reported. Beclab-published images (matched by repository path beclab/... or <registry>/beclab/...) are exempt. This check is OFF by default; enable it with WithSecurityContextCheck() (typically before publishing to the app store). It walks Deployment / StatefulSet / DaemonSet workloads.


5. Version compatibility matrix
olaresManifest.version apiVersion Pipeline Extra rules
Any (including empty / malformed) v1 / empty Parsed as v1 directly, or after template rendering
Any v2 Same as above, still against the v1 schema checkSubCharts active
< 0.12.0 Any dualOwnerPipeline (helm template render in both owner scenarios)
>= 0.12.0 Any singlePipeline (literal parse)
< 0.12.0 Any §1.3 spec validation: legacy flat fields (requiredCpu / limitedCpu / requiredMemory / limitedMemory / requiredDisk) required + quantity-checked; limitedDisk optional; requiredGpu / limitedGpu optional
>= 0.12.0 Any §1.3 spec validation: spec.resources[] required (each entry follows §2.2 base rules); requiredGpu / limitedGpu still optional at the spec level
Any Any §1.3 spec validation (cross-field, version-independent): Rule 7 mutual exclusion (no legacy flat fields when spec.resources[] is set)
>= 0.12.0 Any §1.3 spec validation (cross-field, modern-only): Rule 1 mode → supportArch and Rule 4-empty completeness on every declared spec.resources[] entry. Skipped on < 0.12.0 because spec.resources[] is not part of the legacy schema

These four axes are orthogonal: v2 always triggers checkSubCharts; >=0.12.0 makes spec.resources[] mandatory at the field level; v1 + <0.12.0 does not trigger checkSubCharts and requires the legacy flat fields. Inside specResourceCrossFieldRules, Rule 7 (mutual exclusion) runs regardless of version and only fires when spec.resources[] is non-empty; Rule 1 and Rule 4-empty are gated to >=0.12.0 so the spec.resources[] payload is left untouched on legacy manifests.

Documentation

Overview

Package oachecker lints an OlaresApp (`oac`) chart directory. It validates the OlaresManifest.yaml against its declared apiVersion, dry-runs the helm chart to inspect the resulting workloads, and produces the deduped image list that downstream tooling needs.

A typical caller uses one of the top-level convenience functions:

oac.LintChart("./myapp", oac.WithOwnerAdmin("alice"))
images, err := oac.ListImagesFromOAC("./myapp")

For more control, construct a Checker with New and call its methods directly.

Index

Constants

View Source
const AllModes = "all"

AllModes is the literal keyword that ListImagesForModes / the related top-level shortcuts expand into AllImageRenderModes. Matched case-insensitively so "ALL", "All", "all" all mean the same thing.

View Source
const ManifestFileName = "OlaresManifest.yaml"

ManifestFileName is the well-known file name that holds the OlaresManifest.

View Source
const NewOlaresManifestVersion = "0.12.0"

NewOlaresManifestVersion is the threshold (inclusive) at which the OlaresManifest parsing pipeline switches from the legacy helm-template path to the literal-parse path. Manifests whose olaresManifest.version is at or above this version are considered "new" by IsNewOlaresManifestVersion.

Variables

View Source
var AllImageRenderModes = []string{
	"cpu",
	"apple-m",
	"nvidia",
	"nvidia-gb10",
	"mthreads-m1000",
	"strix-halo",
}

AllImageRenderModes is the ordered list of .Values.GPU.Type values that "all" expands into when ListImagesForModes (or its top-level shortcut) is invoked. Each mode triggers a separate helm render under the matching GPU-Type override; the union of workload images across every render is deduped and returned alongside options.images.

The list intentionally mirrors the resource modes the Olares app store advertises, not the broader resource_mode enum that OlaresManifest validation accepts (validResourceModes). They can diverge over time: a mode may still be a valid manifest value while no longer being part of the default image-extraction set, and vice versa. Callers that want to drive image extraction off the manifest's own spec.resources[] can build their own slice from cfg.Spec.Resources instead of passing "all".

View Source
var ErrNotImplemented = errors.New("not implemented")

ErrNotImplemented is returned by manifest strategies whose validation logic has not been implemented yet (e.g. v2 scaffold).

Functions

func AggregateErrors

func AggregateErrors(errs []error) error

AggregateErrors combines multiple errors into one. Returns nil when the input is empty or all entries are nil. The input slice is not modified.

func IsNewOlaresManifestVersion

func IsNewOlaresManifestVersion(version string) bool

IsNewOlaresManifestVersion reports whether the given olaresManifest.version is at or above the NewOlaresManifestVersion (0.12.0) threshold. Empty or malformed versions return false (treated as legacy).

This is the same predicate downstream tooling (e.g. app-service) needs in order to branch between legacy-only logic and modern-manifest behaviour without re-implementing the semver comparison.

func Lint

func Lint(oacPath string, opts ...Option) error

Lint is the Checker-less shortcut for (*Checker).Lint.

func LintBothOwnerScenarios

func LintBothOwnerScenarios(oacPath string, extraOpts ...Option) error

LintBothOwnerScenarios runs Lint twice: once with owner == admin (cluster admin install) and once with owner != admin (regular user install). Both scenarios must pass.

This is kept as a named shortcut for Lint with WithAutoOwnerScenarios appended to the caller's options.

func ListImagesFromOAC

func ListImagesFromOAC(oacPath string, opts ...Option) ([]string, error)

ListImagesFromOAC is the Checker-less shortcut for (*Checker).ListImages.

func ListImagesFromOACForMode

func ListImagesFromOACForMode(oacPath, mode string, opts ...Option) ([]string, error)

ListImagesFromOACForMode is the Checker-less shortcut for (*Checker).ListImagesForMode.

func ListImagesFromOACForModes

func ListImagesFromOACForModes(oacPath string, modes []string, opts ...Option) ([]string, error)

ListImagesFromOACForModes is the Checker-less shortcut for (*Checker).ListImagesForModes. Use it when you want to extract images across several GPU-Type modes (or the "all" keyword) without holding a Checker yourself.

func ValidateAppConfiguration

func ValidateAppConfiguration(cfg *AppConfiguration, opts ...Option) error

ValidateAppConfiguration is the Checker-less shortcut for one-off callers. Options behave identically to the Checker form (today only SkipManifestCheck is meaningful here; the rest don't apply to a bare *AppConfiguration).

func ValidateManifestContent

func ValidateManifestContent(content []byte, opts ...Option) error

ValidateManifestContent is the byte-slice counterpart of ValidateManifestFile.

func ValidateManifestFile

func ValidateManifestFile(oacPath string, opts ...Option) error

ValidateManifestFile is the Checker-less shortcut for one-off callers.

func WrapValidation

func WrapValidation(version string, err error) error

WrapValidation converts an ozzo validation.Errors map into a sorted, stable, multi-line ValidationError. If err is nil it returns nil. If err is not a validation.Errors it is wrapped as a single-message ValidationError.

Types

type AppConfiguration

type AppConfiguration = apimanifest.AppConfiguration

AppConfiguration is the parsed OlaresManifest.yaml payload. It is a direct type alias onto github.com/beclab/api/manifest.AppConfiguration, so reading every field works out of the box:

cfg, _ := oac.LoadAppConfiguration("./myapp")
for _, e := range cfg.Entrances { fmt.Println(e.Name, e.Port) }

When you also need to construct sub-structs (AppMetaData, Entrance, Options, ResourceMode, …) — for tests, generators, or programmatic edits — import github.com/beclab/api/manifest (and, for Entrance and friends, github.com/beclab/api/api/app.bytetrade.io/v1alpha1) directly:

import (
    "github.com/beclab/oac"
    "github.com/beclab/api/manifest"
    appv1 "github.com/beclab/api/api/app.bytetrade.io/v1alpha1"
)
cfg.Spec.SubCharts = append(cfg.Spec.SubCharts, manifest.Chart{...})
cfg.Entrances      = append(cfg.Entrances,      appv1.Entrance{...})

Because everything is type-aliased (not a new named type), values flow freely between oac and the upstream api packages without conversion.

func AsAppConfiguration

func AsAppConfiguration(m Manifest) (*AppConfiguration, bool)

AsAppConfiguration unwraps a Manifest into the concrete *AppConfiguration. The second return value is false when the manifest was produced by a future Strategy whose Raw() type is not AppConfiguration; today every Strategy in the tree returns one, so the boolean is a forward-compat hook rather than a runtime concern.

func LoadAppConfiguration

func LoadAppConfiguration(oacPath string, opts ...Option) (*AppConfiguration, error)

LoadAppConfiguration is the Checker-less shortcut for one-off callers.

func LoadAppConfigurationContent

func LoadAppConfigurationContent(content []byte, opts ...Option) (*AppConfiguration, error)

LoadAppConfigurationContent is the byte-slice counterpart.

type CustomValidator

type CustomValidator func(oacPath string, m Manifest) error

CustomValidator is invoked with the chart directory path and the parsed Manifest after the built-in structural checks have run.

type EntranceInfo

type EntranceInfo = manifest.EntranceInfo

EntranceInfo is a lightweight view of a manifest entrance shared between versions.

type Manifest

type Manifest = manifest.Manifest

Manifest is the cross-version, read-only view of a parsed OlaresManifest. Raw() yields *github.com/beclab/api/manifest.AppConfiguration (same type as oac.AppConfiguration — the two are type-aliased together).

type ManifestResourceLimits

type ManifestResourceLimits = manifest.ResourceRequirementLimits

ManifestResourceLimits is the full resource envelope (CPU, memory, disk, GPU required/limited pairs) for one spec.resources[] mode row.

func ResourceLimitsForResourceMode

func ResourceLimitsForResourceMode(cfg *AppConfiguration, mode string) (ManifestResourceLimits, error)

ResourceLimitsForResourceMode returns required/limited CPU, memory, disk, and GPU for the spec.resources[] element whose mode matches (case-insensitive). The inline ResourceRequirement on the matched row is returned verbatim — empty fields stay empty.

type ManifestVersions

type ManifestVersions struct {
	APIVersion            string
	OlaresManifestVersion string
}

ManifestVersions is the lightweight pair of top-level version fields read from raw OlaresManifest.yaml before full parsing. APIVersion is the value after the apiVersion key; OlaresManifestVersion is the value after olaresManifest.version.

func PeekManifestVersions

func PeekManifestVersions(content []byte) (ManifestVersions, error)

PeekManifestVersions extracts apiVersion and olaresManifest.version from content using the same line-oriented probe as the manifest parsing pipeline (tolerates unrendered Helm template fragments in the scalar). Missing keys yield empty strings; only I/O-style scanner failures produce a non-nil error.

type OAC

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

OAC is a reusable lint context. All fields are private; build one via New(opts...) and the With*/Skip* option helpers.

func New

func New(opts ...Option) *OAC

New builds a Checker with the given options applied on top of the default configuration:

  • no owner/admin override (templates fall back to the "default" placeholder)
  • owner/admin scenarios are NOT auto-expanded — the explicit owner/admin values are used as-is (see WithAutoOwnerScenarios)
  • all built-in checks enabled EXCEPT ServiceAccount rule inspection (RBAC is off by default; the Chart.yaml <-> manifest same-version check runs by default — turn it off with SkipSameVersionCheck; the .Values.userspace.appdata template-vs-manifest cross-check runs by default — turn it off with SkipAppDataCheck; the hostPath + rolling-update incompatibility check runs by default — turn it off with SkipHostPathCheck; the rendered-resource namespace check runs by default — turn it off with SkipResourceNamespaceCheck)

func (*OAC) Admin

func (c *OAC) Admin() string

Admin returns the configured .Values.admin value, or "" when unset.

func (*OAC) CheckChartFolder

func (c *OAC) CheckChartFolder(oacPath string) error

CheckChartFolder validates that oacPath is a structurally-valid chart directory (Chart.yaml/values.yaml/templates/OlaresManifest.yaml present, folder name well-formed).

func (*OAC) CheckResources

func (c *OAC) CheckResources(oacPath string) error

CheckResources dry-runs the chart and performs the resource-list level limit check. The manifest is parsed implicitly.

apiVersion v2 skips this check entirely (returns nil). v1, v3, and empty apiVersion (v1 default) share the same logic: one helm render at oacPath for the legacy path, and per-mode renders at oacPath for modern manifests. A non-empty apiVersion outside v1/v2/v3 yields not supported version.

For legacy manifests (<0.12.0) the chart is rendered once and the container-level limits are compared against spec.required*/spec.limited*. For modern manifests (>=0.12.0) limits come from spec.resources[]; each mode drives its own helm render with .Values.GPU.Type set to rm.Mode.

func (*OAC) CheckSameVersion

func (c *OAC) CheckSameVersion(oacPath string, m Manifest) error

CheckSameVersion cross-validates the folder name, Chart.yaml metadata, and parsed manifest metadata. Provide nil for m to have it loaded on demand.

func (*OAC) CheckServiceAccountRules

func (c *OAC) CheckServiceAccountRules(oacPath string) error

CheckServiceAccountRules inspects Role/ClusterRole bindings in the rendered chart and returns an error if any of them grants the ServiceAccount one of the built-in forbidden permissions.

func (*OAC) Lint

func (c *OAC) Lint(oacPath string) error

Lint runs the full lint pipeline against oacPath. The exact set of checks executed depends on the Skip* options set on the Checker:

  1. Folder layout (chartfolder.CheckLayout) - skipped by SkipFolderCheck
  2. Manifest parse + ozzo validation - skipped by SkipManifestCheck
  3. Built-in .Values.userspace.appdata cross-check - skipped by SkipAppDataCheck (on by default)
  4. Custom validators registered via WithCustomValidator (none by default)
  5. Helm dry-run and mandatory workload-integrity checks (upload mount path, `type=app` workload naming) - ALWAYS run; not governed by any Skip* option
  6. HostPath + rolling-update incompatibility check - ON by default, turn off with SkipHostPathCheck()
  7. Rendered-resource namespace check (workloads in app-namespace; other resources in app-namespace or user-system-*) - ON by default, turn off with SkipResourceNamespaceCheck()
  8. Container-level resource limits check - skipped by SkipResourceCheck
  9. Chart.yaml <-> manifest same-version check - ON by default, turn off with SkipSameVersionCheck()
  10. ServiceAccount RBAC inspection - OFF by default, turn on with WithServiceAccountRulesCheck()
  11. Non-beclab image privileged securityContext check - OFF by default, turn on with WithSecurityContextCheck()

When WithAutoOwnerScenarios() is set, every owner-dependent step runs twice — once with owner == admin and once with owner != admin. That covers the rendered-chart steps (5/6/7/8/10) AND step 2's manifest validation, so manifests that branch on `eq .Values.admin .Values.bfl.username` are exercised in both install modes. Owner-independent steps (folder layout, appdata cross-check, same-version) still run once.

func (*OAC) ListImages

func (c *OAC) ListImages(oacPath string) ([]string, error)

ListImages returns the sorted, deduplicated set of container images used by oacPath. The set is the union of:

  1. Images discovered by walking the Deployment/StatefulSet/DaemonSet workloads produced by a helm dry-run (primary containers only).
  2. Images listed under options.images in OlaresManifest.yaml, which is how apps declare extra images that are pulled outside the chart (e.g. images referenced at runtime or by client-side tooling).

ListImages is the no-mode shortcut for ListImagesForMode -- the chart is rendered without any .Values.GPU.Type override, which surfaces the images of the chart's default (non-GPU) branch only.

func (*OAC) ListImagesForMode

func (c *OAC) ListImagesForMode(oacPath, mode string) ([]string, error)

ListImagesForMode is the mode-aware variant of ListImages: it renders the chart with .Values.GPU.Type set to mode so chart templates that branch per GPU family (e.g. {{ if eq .Values.GPU.Type "nvidia" }}) emit the matching workload set. The returned list is still the union of those rendered workload images and options.images, sorted and deduplicated.

Passing an empty mode is identical to calling ListImages: no GPU.Type override is injected and the default branch of the chart renders.

func (*OAC) ListImagesForModes

func (c *OAC) ListImagesForModes(oacPath string, modes []string) ([]string, error)

ListImagesForModes returns the union of container images across each .Values.GPU.Type mode in modes. The chart is helm-rendered once per expanded mode, the resulting Deployment/StatefulSet workload images are collected, then unioned with the manifest's options.images and returned as a sorted, deduplicated slice.

Mode semantics:

  • A nil / empty modes slice is treated as a single render with no GPU.Type override, identical to ListImages.
  • An empty string entry renders the chart's default branch (no override), same as ListImages.
  • Any element equal to AllModes ("all", case-insensitive) expands in-place into AllImageRenderModes. Duplicates introduced by mixing "all" with explicit modes are collapsed, so each mode renders at most once per call.
  • Other entries are passed straight through as the .Values.GPU.Type value for that mode's render.

Errors from any single render fail the whole call and identify the offending mode in the wrapping message.

func (*OAC) LoadAppConfiguration

func (c *OAC) LoadAppConfiguration(oacPath string) (*AppConfiguration, error)

LoadAppConfiguration reads OlaresManifest.yaml from oacPath, runs it through the version-aware parsing pipeline, and returns the concrete *AppConfiguration. No validation is performed — pair with ValidateManifestFile or ValidateAppConfiguration if you also want structural checks. Legacy manifests (<0.12.0) are template-rendered with the Checker's owner/admin before parsing; modern (>=0.12.0) manifests are parsed verbatim.

func (*OAC) LoadAppConfigurationContent

func (c *OAC) LoadAppConfigurationContent(content []byte) (*AppConfiguration, error)

LoadAppConfigurationContent is the byte-slice counterpart of LoadAppConfiguration.

func (*OAC) LoadManifestContent

func (c *OAC) LoadManifestContent(content []byte) (Manifest, error)

LoadManifestContent is the byte-slice counterpart of LoadManifestFile.

func (*OAC) LoadManifestFile

func (c *OAC) LoadManifestFile(oacPath string) (Manifest, error)

LoadManifestFile reads OlaresManifest.yaml from oacPath and returns the parsed manifest. Legacy (<0.12.0) payloads are template-rendered with the checker's owner/admin before parsing; modern (>=0.12.0) payloads are parsed verbatim. No validation is performed here — use ValidateManifestFile for that.

func (*OAC) Owner

func (c *OAC) Owner() string

Owner returns the configured .Values.bfl.username value, or "" when unset.

func (*OAC) ValidateAppConfiguration

func (c *OAC) ValidateAppConfiguration(cfg *AppConfiguration) error

ValidateAppConfiguration runs structural and cross-field validation on an already-parsed manifest. It applies the same rules as ValidateManifestFile / ValidateManifestContent but starts from an in-memory *AppConfiguration instead of raw YAML — no chart rendering, no folder check, no custom validators (those need an oacPath and the chart templates, which a bare *AppConfiguration cannot supply).

Respects SkipManifestCheck(): when set, the method returns nil without running any rule, consistent with how Lint and ValidateManifestFile react to the same option.

A nil cfg is treated as a validation failure rather than a panic. Failures are wrapped as *ValidationError keyed off cfg.APIVersion (defaults to "v1" when empty, matching the parsing pipeline).

func (*OAC) ValidateManifestContent

func (c *OAC) ValidateManifestContent(content []byte) error

ValidateManifestContent is the byte-slice counterpart of ValidateManifestFile. It honors WithAutoOwnerScenarios() the same way (manifest validation runs once per owner scenario).

func (*OAC) ValidateManifestFile

func (c *OAC) ValidateManifestFile(oacPath string) error

ValidateManifestFile parses and validates oacPath/OlaresManifest.yaml. No chart rendering is performed. For legacy manifests (<0.12.0) the underlying pipeline re-parses the payload under both admin=owner and admin!=owner scenarios and aggregates any failures into a single ValidationError.

When WithAutoOwnerScenarios() is set, the manifest validation is repeated for each (owner, admin) pair (owner==admin / owner!=admin) so manifests whose body branches on `eq .Values.admin .Values.bfl.username` are exercised in both configurations. Failures from each scenario are aggregated into a single *ValidationError.

type Option

type Option func(*OAC)

Option mutates a Checker built via New. Options are idempotent and safe to apply in any order.

func SkipAppDataCheck

func SkipAppDataCheck() Option

SkipAppDataCheck disables the built-in template-vs-manifest cross-check that scans chart templates for .Values.userspace.appdata references and requires permission.appData in OlaresManifest.yaml when any are found. The check is enabled by default; only opt out when a caller knowingly renders appdata via a non-standard path.

func SkipFolderCheck

func SkipFolderCheck() Option

SkipFolderCheck disables the chart-folder layout check.

func SkipHostPathCheck

func SkipHostPathCheck() Option

SkipHostPathCheck disables the built-in hostPath + rolling-update incompatibility check. The check is on by default since combining a hostPath volume with a rolling update silently produces broken installations (the new pod can't see the old node's host directory); only opt out when a chart legitimately handles this in another way.

func SkipManifestCheck

func SkipManifestCheck() Option

SkipManifestCheck disables OlaresManifest.yaml structural validation.

func SkipResourceCheck

func SkipResourceCheck() Option

SkipResourceCheck disables the container-level resource-limits check.

Note: this option does NOT disable the upload-mount and workload-naming checks, which Lint always runs because they guard structural integrity (a chart that declares options.upload.dest but mounts it nowhere, or an app whose templates produce no Deployment/StatefulSet named after the app, is broken regardless of limit accounting).

func SkipResourceNamespaceCheck

func SkipResourceNamespaceCheck() Option

SkipResourceNamespaceCheck disables the built-in rendered-resource namespace check. The check enforces that Deployment/StatefulSet/DaemonSet workloads land in "app-namespace" and that other namespaced resources land in "app-namespace" or a "user-system-*" namespace; cluster-scoped resources are skipped. It is on by default; only opt out when a chart legitimately renders resources into a different namespace.

func SkipSameVersionCheck

func SkipSameVersionCheck() Option

SkipSameVersionCheck disables the Chart.yaml <-> manifest version consistency check. By default the check runs; callers that roll their own version-alignment step can opt out here.

func SkipSecurityContextCheck

func SkipSecurityContextCheck() Option

SkipSecurityContextCheck clears the non-beclab image securityContext flag. The check is OFF by default, so calling this on a fresh Checker is a no-op; it exists for option-set composition where a previously applied set may have turned the check on.

func WithAdmin

func WithAdmin(admin string) Option

WithAdmin sets the .Values.admin template value. An empty admin is ignored.

func WithAppDataValidator

func WithAppDataValidator() Option

WithAppDataValidator re-enables the built-in .Values.userspace.appdata cross-check after a previous option set (re)disabled it. The check is on by default since it is essentially a safety net against permission misconfiguration, so calling this on a fresh Checker is a no-op. Kept as a named option for backward compatibility — old call sites that used it to "register" the validator continue to compile and behave as before, modulo the fact that the check is no longer wired through customValidators (so it runs exactly once even when this option is passed multiple times).

func WithAutoOwnerScenarios

func WithAutoOwnerScenarios() Option

WithAutoOwnerScenarios makes Lint / ValidateManifestFile / ValidateManifestContent ignore any explicit WithOwner / WithAdmin / WithOwnerAdmin values and instead run every owner-dependent step twice:

  1. owner == admin (cluster-admin install)
  2. owner != admin (regular user install)

This covers:

  • The chart-rendering portion of Lint (helm dry-run + workload integrity checks, container resource limits, RBAC inspection).
  • The manifest structural validation (validateManifestBytes), so OlaresManifest.yaml bodies that branch on `eq .Values.admin .Values.bfl.username` are exercised in both configurations.

Both scenarios must pass; failures are aggregated. Owner-independent steps (folder layout, appdata cross-check, same-version) still run once.

This is the programmatic equivalent of the LintBothOwnerScenarios helper — use it whenever the caller does not have a concrete owner/admin pair and wants the linter to cover both install modes automatically.

func WithCustomValidator

func WithCustomValidator(fn CustomValidator) Option

WithCustomValidator adds a user-defined validator to the Checker.

func WithHostPathCheck

func WithHostPathCheck() Option

WithHostPathCheck re-enables the built-in hostPath + rolling-update incompatibility check after a previous option set disabled it. The check is on by default, so calling this on a fresh Checker is a no-op.

func WithOwner

func WithOwner(owner string) Option

WithOwner sets the .Values.bfl.username template value and the owner field used when rendering helm charts. When owner is empty the Checker keeps its existing value.

func WithOwnerAdmin

func WithOwnerAdmin(value string) Option

WithOwnerAdmin sets both owner and admin to the same value, modelling the "installed as admin" scenario where the cluster administrator is also the acting user.

func WithResourceNamespaceCheck

func WithResourceNamespaceCheck() Option

WithResourceNamespaceCheck re-enables the built-in rendered-resource namespace check after a previous option set disabled it. The check is on by default, so calling this on a fresh Checker is a no-op.

func WithSameVersionCheck

func WithSameVersionCheck() Option

WithSameVersionCheck re-enables the Chart.yaml <-> manifest version consistency check. Mostly useful when composing an option set that had SkipSameVersionCheck baked in and a particular call-site wants it back on.

func WithSecurityContextCheck

func WithSecurityContextCheck() Option

WithSecurityContextCheck enables the non-beclab image privileged securityContext check. The check rejects any container (init or main) whose image is NOT published under the beclab/ namespace and whose effective securityContext grants root-equivalent privileges (any of `privileged: true`, `runAsUser: 0`, `runAsNonRoot: false`, including the value inherited from a pod-level securityContext). It is disabled by default because some legacy charts still embed third-party images that need a manual review before this rule applies; turn it on explicitly when publishing to the app store.

func WithServiceAccountRulesCheck

func WithServiceAccountRulesCheck() Option

WithServiceAccountRulesCheck enables the RBAC rule inspection which makes sure the chart doesn't grant ServiceAccounts forbidden permissions. It is disabled by default to match historical Lint behaviour; callers that need it can opt in explicitly.

func WithValues

func WithValues(extra map[string]interface{}) Option

WithValues registers extra helm values that the Checker deep-merges on top of the scaffold produced by helmrender.BuildValues for every render it performs (Lint, ListImages / ListImagesForMode, CheckResources, CheckServiceAccountRules, ...). External keys win on conflicts: scalar keys are replaced wholesale, and when both sides are maps the merge recurses so siblings the caller did not override are preserved.

Multiple WithValues calls are additive -- each is merged into the already-accumulated extra-values map under the same precedence rules. Passing nil is a no-op.

The mode argument of ListImagesForMode and the per-mode loop in resource-limit checks always set .Values.GPU.Type AFTER WithValues is applied, so they keep winning over any GPU.Type the caller injected.

func WithoutAutoOwnerScenarios

func WithoutAutoOwnerScenarios() Option

WithoutAutoOwnerScenarios clears the auto-owner flag, pinning Lint back to the explicit owner/admin values. Mostly useful when composing option sets that have WithAutoOwnerScenarios baked in and a particular call-site wants to opt out.

type ValidationError

type ValidationError struct {
	// Version is the manifest version (apiVersion) that produced the error,
	// e.g. "v1" or "v2".
	Version string
	// Field is the dotted field path that failed, e.g. "metadata.name".
	Field string
	// Reason is a short human-readable explanation.
	Reason string
	// Inner is the underlying error if any (typically validation.Errors from ozzo).
	Inner error
}

ValidationError describes a single failed manifest validation.

Callers should use errors.As to pull this out of the error chain returned by ValidateManifestFile / ValidateManifestContent / LintChart and friends.

func NewValidationError

func NewValidationError(version, field, reason string) *ValidationError

NewValidationError constructs a single-field ValidationError.

func (*ValidationError) Error

func (e *ValidationError) Error() string

func (*ValidationError) Unwrap

func (e *ValidationError) Unwrap() error

Directories

Path Synopsis
internal
chartfolder
Package chartfolder implements the structural checks that verify a chart directory is well-formed (Chart.yaml / values.yaml / templates/ / OlaresManifest.yaml present) and that its metadata is consistent with the parsed manifest.
Package chartfolder implements the structural checks that verify a chart directory is well-formed (Chart.yaml / values.yaml / templates/ / OlaresManifest.yaml present) and that its metadata is consistent with the parsed manifest.
helmrender
Package helmrender wraps helm's dry-run engine with a set of sensible fake values so oac can lint charts without talking to a real cluster.
Package helmrender wraps helm's dry-run engine with a set of sensible fake values so oac can lint charts without talking to a real cluster.
resources
Package resources hosts cross-version resource-level checks that run over the kube.ResourceList produced by helmrender.Render.
Package resources hosts cross-version resource-level checks that run over the kube.ResourceList produced by helmrender.Render.

Jump to

Keyboard shortcuts

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