Help us shape the future of Go! Take the 2026 Go Developer Survey

kcleval

package
v1.0.14 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package kcleval evaluates ONE KCL file of a project and selects fields out of the result, so nothing outside forge ever has to run `kcl` itself.

The gap this closes

Since forge started supplying the `forge` KCL module from the binary (docs/adr/0003-kcl-module-from-the-binary.md), a plain `kcl run` on a file that says `import forge` CANNOT resolve it: there is no kcl.mod dependency to fetch and no project-local copy to read. That is deliberate — the binary rendering a project IS the module version it renders against — and it is also why a consumer with a legitimate need to read a value out of project KCL had nowhere to go.

forge's answer was `forge env render <env>`, which evaluates an ENVIRONMENT and prints Kubernetes objects. A project also keeps plain declarative constants in library files — control-plane's `deploy/kcl/lib/kata_pool.k` declares GKE node-pool inputs, its `lib/daemon_placement.k` declares which cluster each env's daemon pods land in — and those are read by shell scripts and Go tests, not deployed. There was no forge command that evaluated an arbitrary FILE and handed back one field.

So consumers reconstructed forge's own render setup from outside: a control-plane shell library made forge materialize its module into FORGE_KCL_MODULE_CACHE, dug the directory out, and ran `kcl run <file> -S <field> -E forge=<dir>`. That works right up until any part of forge's render setup changes — the cache layout, the module name, the plugin namespace — at which point it breaks in a consumer's CI with an error about KCL, naming nothing that would lead anyone back here. It also requires `kcl` on PATH, which forge itself has not needed since it embedded the runtime.

What this is NOT

Not an env render. Nothing here resolves an image tag, reads build state, routes objects to clusters, or templates a helm chart — see internal/cli/env_render.go for that. The unit is a file and the output is the file's own values.

Not a port claim. Evaluation arms the read-only halves of the render context (internal/cli.armReadOnlyKCLContext): `fp.allocate_port` resolves keys to blocks they already hold and `fp.resolve_port` reads its store without writing it, so evaluating a file can neither hand out a port block nor fail at the dev_stack ceiling. A read-only command never claims a port block.

Why selection happens HERE and not in kcl

kcl's own `-S` renders a selected LIST as one YAML document per element, which is unusable without knowing how many elements to expect; control-plane's check-kata-pool-staleness.sh carries a hand-written workaround for exactly this, and a comment explaining that an empty result passes a tier name through as data. Selecting in Go against the parsed JSON gives back the value KCL actually produced — an array stays one array — and it is the same traversal for a scalar, a list and a schema instance.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoSuchField = errors.New("no such field")

ErrNoSuchField is returned when a selector names a field the evaluated document does not have. It is a distinct error because a caller reading an optional field wants to tell "absent" from "the file does not evaluate", and because a typo'd selector must not read as an empty value — the failure mode that made a kata-pool script pass a tier name through as data.

Functions

func Decode added in v1.0.0

func Decode(raw []byte) (any, error)

Decode parses KCL's JSON result into a document that keeps key order.

func Formats

func Formats() []string

Formats are the accepted --format values, for the flag's help and its refusal message.

func Render

func Render(r Result, f Format) ([]byte, error)

Render turns a Result into the bytes to print.

Why FormatRaw exists

It is the format that removes the reason anyone reached for `kcl` directly. `kcl run -S <field>` prints YAML, and YAML QUOTES a scalar that would otherwise parse as something else — so every caller wanting a plain string wrote a cleanup pipeline. control-plane's build-workspace-sbd.sh carries six of them:

BUILDER_REPO="$(kcl run -S sbd.builder_repo "${KCL}" | tr -d "\n'\"")"

That `tr` deletes every newline, single quote and double quote ANYWHERE in the value, so a value legitimately containing one comes back silently corrupted, and the pipeline is why nobody noticed the quoting rule differed per value. FormatRaw prints the scalar's own text and nothing else — no quotes, no trailing newline — so `$(...)` captures exactly the value:

BUILDER_REPO="$(forge kcl eval "${KCL}" -S sbd.builder_repo --format raw)"

A number prints as KCL's own text: `-S sbd.disk_size_gb` must produce `200` for a caller passing it to `gcloud --boot-disk-size`, and `200` is what the KCL says. The document keeps numbers as json.Number rather than float64, so nothing is reformatted on the way through.

It refuses a composite rather than falling back to JSON. A caller asking for raw wants a value it can interpolate into a command line; handing it `{"a":1}` would interpolate cleanly and mean nothing, and the failure would surface as whatever that command did with a JSON blob. Naming the problem at the boundary is the whole value of a typed refusal here.

func Select

func Select(doc any, path string) (any, error)

Select walks a dotted path through a decoded KCL document.

A numeric segment indexes a list, so `pools.0.name` reaches into one, and a map key that happens to be numeric still wins over the list reading — checked in that order because a map is the common case and a KCL map may legitimately be keyed "0".

Types

type Format

type Format string

Format is how a Result is printed.

const (
	// FormatJSON is the default: the value as JSON, indented.
	FormatJSON Format = "json"
	// FormatYAML is the value as YAML, which is what kcl's own `-S` prints.
	FormatYAML Format = "yaml"
	// FormatRaw is a scalar's own text, with no quoting and no trailing
	// newline. See Render.
	FormatRaw Format = "raw"
)

type Member added in v1.0.0

type Member struct {
	Key   string
	Value any
}

Member is one key of an Object.

type Object added in v1.0.0

type Object []Member

Object is a KCL dict or schema instance with its keys in the order KCL declared them.

It exists because the render arrives as JSON in declared order and a Go map has none: decoding into map[string]any alphabetised every `--format json` document, so a consumer comparing committed JSON byte for byte could not use forge's output and needed a second `kcl run` pass. KCL cannot repeat a key, so a slice of members loses nothing a map held.

A decoded document holds only Object, []any, string, json.Number, bool and nil. Numbers stay json.Number — KCL's own text — so `1.0` and `1e+20` print as KCL wrote them rather than as a float64 would.

type Request

type Request struct {
	// ProjectDir is the project root (the directory holding forge.yaml). The
	// kcl.mod migration checks read it, and File must resolve inside it.
	ProjectDir string
	// File is the .k file to evaluate, relative to ProjectDir or absolute.
	File string
	// Selectors are dotted field paths to take out of the result, in the
	// order given. Empty selects the whole document.
	Selectors []string
	// Options are `key=value` top-level option bindings, as `-D` passes
	// them to kcl.
	Options []string
	// ProcessRooted is for an isolated CLI child already started with its cwd
	// at ProjectDir. It avoids even a temporary process-global chdir.
	ProcessRooted bool
}

Request is one evaluation: a file, the selections to take out of it, and the `-D` option bindings to evaluate it under.

type Result

type Result struct {
	// Value is the selected value: the whole document when no selector was
	// given, the selected value for exactly one, and an object keyed by
	// selector for several. It is built from Object, []any, string,
	// json.Number, bool and nil, so key order and number text survive.
	Value any
	// Scalar reports whether Value is a single non-composite value, which is
	// what --format raw is allowed to print.
	Scalar bool
}

Result is what an evaluation produced.

func Eval

func Eval(req Request) (Result, error)

Eval evaluates req.File and applies req.Selectors.

The evaluation runs with the PROJECT ROOT as the working directory, exactly as `env render` does, so one project has one evaluation semantics regardless of which file is being evaluated or where the caller stands. Two things follow, and they are the two the shell scripts this replaces had to know about:

  • A relative `lib.*` import resolves from the file's own kcl.mod package, because that is how kpm resolves a package — not from the cwd. The scripts' `cd deploy/kcl` was never what made their imports work.
  • A `file.read("deploy/...")` resolves from the project root, because the KCL runtime reads relative paths against the process cwd. THAT is what the `cd` was load-bearing for, and getting it wrong fails with "No such file or directory" naming a path that plainly exists.

A caller in a subdirectory therefore gets the same answer as one at the project root — the property `forge -C <dir>` was missing (see kclrender.Run's chdir note).

Jump to

Keyboard shortcuts

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