mirror of
https://github.com/holos-run/holos.git
synced 2026-03-22 10:15:01 +00:00
Extract YAML is more clear and aligns with the schema docs for the Component Instance field which has an extractYAML kind. This also leaves the door open for additional kinds of data extractors which are almost certainly going to be needed.
192 lines
4.4 KiB
Go
192 lines
4.4 KiB
Go
package builder
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"cuelang.org/go/cue"
|
|
"cuelang.org/go/cue/cuecontext"
|
|
"cuelang.org/go/cue/load"
|
|
"cuelang.org/go/encoding/yaml"
|
|
"github.com/holos-run/holos/internal/errors"
|
|
"github.com/holos-run/holos/internal/holos"
|
|
"github.com/holos-run/holos/internal/util"
|
|
)
|
|
|
|
// ExtractYAML extracts yaml encoded data from file paths. The data is unified
|
|
// into one [cue.Value]. If a path element is a directory, all files in the
|
|
// directory are loaded non-recursively.
|
|
//
|
|
// Attribution: https://github.com/cue-lang/cue/issues/3504
|
|
func ExtractYAML(ctxt *cue.Context, filepaths []string) (cue.Value, error) {
|
|
value := ctxt.CompileString("")
|
|
files := make([]string, 0, 10*len(filepaths))
|
|
|
|
for _, path := range filepaths {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return value, errors.Wrap(err)
|
|
}
|
|
|
|
if !info.IsDir() {
|
|
files = append(files, path)
|
|
continue
|
|
}
|
|
|
|
entries, err := os.ReadDir(path)
|
|
if err != nil {
|
|
return value, errors.Wrap(err)
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
files = append(files, filepath.Join(path, entry.Name()))
|
|
}
|
|
}
|
|
|
|
for _, file := range files {
|
|
f, err := yaml.Extract(file, nil)
|
|
if err != nil {
|
|
return value, errors.Wrap(err)
|
|
}
|
|
value = value.Unify(ctxt.BuildFile(f))
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
// LoadInstance loads the cue configuration instance at path. External data
|
|
// file paths are loaded by calling [ExtractYAML] providing filepaths. The
|
|
// extracted data values are unified with the platform configuration [cue.Value]
|
|
// in the returned [Instance].
|
|
func LoadInstance(path string, filepaths []string, tags []string) (*Instance, error) {
|
|
root, leaf, err := util.FindRootLeaf(path)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err)
|
|
}
|
|
|
|
cfg := &load.Config{
|
|
Dir: root,
|
|
ModuleRoot: root,
|
|
Tags: tags,
|
|
}
|
|
ctxt := cuecontext.New()
|
|
|
|
bis := load.Instances([]string{path}, cfg)
|
|
values, err := ctxt.BuildInstances(bis)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err)
|
|
}
|
|
|
|
value, err := ExtractYAML(ctxt, filepaths)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err)
|
|
}
|
|
// TODO: https://cuelang.org/docs/howto/place-data-go-api/
|
|
value = value.Unify(values[0])
|
|
|
|
inst := &Instance{
|
|
path: leaf,
|
|
ctx: ctxt,
|
|
cfg: cfg,
|
|
value: value,
|
|
}
|
|
|
|
return inst, nil
|
|
}
|
|
|
|
// Instance represents a cue instance to build. Use LoadInstance to create a
|
|
// new Instance.
|
|
type Instance struct {
|
|
path string
|
|
ctx *cue.Context
|
|
cfg *load.Config
|
|
value cue.Value
|
|
}
|
|
|
|
// HolosValue returns the value of the holos field of the exported CUE instance.
|
|
func (i *Instance) HolosValue() (v cue.Value, err error) {
|
|
v = i.value.LookupPath(cue.ParsePath("holos"))
|
|
if err = v.Err(); err != nil {
|
|
if strings.HasPrefix(err.Error(), "field not found") {
|
|
slog.Warn(fmt.Sprintf("%s: deprecated usage: nest output under holos: %s", err, i.path), "err", err)
|
|
// Return the deprecated value at the root
|
|
return i.value, nil
|
|
}
|
|
err = errors.Wrap(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Discriminate calls the discriminate func for side effects. Useful to switch
|
|
// over the instance kind and apiVersion.
|
|
func (i *Instance) Discriminate(discriminate func(tm holos.TypeMeta) error) error {
|
|
v, err := i.HolosValue()
|
|
if err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
|
|
var tm holos.TypeMeta
|
|
|
|
kind := v.LookupPath(cue.ParsePath("kind"))
|
|
if err := kind.Err(); err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
if tm.Kind, err = kind.String(); err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
|
|
version := v.LookupPath(cue.ParsePath("apiVersion"))
|
|
if err := version.Err(); err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
if tm.APIVersion, err = version.String(); err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
|
|
if err := discriminate(tm); err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *Instance) Decoder() (*json.Decoder, error) {
|
|
v, err := i.HolosValue()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err)
|
|
}
|
|
|
|
jsonBytes, err := v.MarshalJSON()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err)
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
|
|
decoder.DisallowUnknownFields()
|
|
return decoder, nil
|
|
}
|
|
|
|
func (i *Instance) Export(enc holos.Encoder) error {
|
|
v, err := i.HolosValue()
|
|
if err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
|
|
var data interface{}
|
|
if err := v.Decode(&data); err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
|
|
if err := enc.Encode(&data); err != nil {
|
|
return errors.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|