Compare commits

..

8 Commits

Author SHA1 Message Date
Jeff McCune
0e95a2812e cmd: expose MakeMain() for testing
I'd like to add the kargo-demo repository to Unity to test evalv3, but
can't get a handle on the main function to wire up to testscript.

This patch fixes the problem by moving the MakeMain function to a public
package so the kargo-demo go module can import and call it using the go
mod tools technique.
2024-12-19 15:19:46 -08:00
Jeff McCune
54efe3e24a core: pass --extract-yaml flag from platform to component (#376)
Previously holos render platform was not setting the --extract-yaml file
when calling holos render component, causing data file instances defined
in the Platform spec to be discarded.

This patch passes the value along using the flag.
2024-12-19 08:39:55 -08:00
Jeff McCune
f693f049f4 core: refactor --instance to --extract-yaml (#376)
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.
2024-12-19 08:34:05 -08:00
Jeff McCune
85238710ac core: unify data files into config (#376)
Previously there isn't a good way to unify json and yaml files with the
cue configuration.  This is a problem for use cases where data can be
generated idempotentialy prior to rendering the platform configuration.

The first use case is to explore unifying configuration with decrypted
sops values, which isn't typical since Holos is designed to handle
secrets with ExternalSecret resources, but does fit into the use case of
executing a command to produce data idempotently, then make the data
available to the platform configuration.

Other use cases this feature is intended to support are the prior
experiment where we fetch top level platform configuration from an rpc
service, and the future goal of integrating with data provided by
Terraform.
2024-12-19 08:34:05 -08:00
Jeff McCune
3ec62d272e v1alpha5: update kargo crds to 1.1.1 2024-12-19 08:34:04 -08:00
Jeff McCune
49afb44fd4 docs: redirect /docs/comparison/ 2024-12-18 14:37:36 -08:00
Gary Larizza
a023f135ab Add a Comparisons page
PROBLEM:

We've noticed that Holos almost immediately gets compared to Timoni, and
we frequently get asked for specifics in how they're similar/different.

SOLUTION:

* Add a `Comparison` page.
* Include a section that compares Holos to Timoni

OUTCOME:

Fewer questions about how Holos compares to Timoni because people are
able to find that answer themselves on our docs page.
2024-12-18 14:33:52 -08:00
Jeff McCune
c6a3a5d689 docs: redirect /docs/kargo/ 2024-12-17 06:30:20 -08:00
24 changed files with 557 additions and 139 deletions

View File

@@ -74,6 +74,7 @@
"creds",
"crossplane",
"crunchydata",
"ctxt",
"cuecontext",
"cuelang",
"customresourcedefinition",

View File

@@ -303,6 +303,10 @@ type Component struct {
// Path represents the path of the component relative to the platform root.
// Injected as the tag variable "holos_component_path".
Path string `json:"path" yaml:"path"`
// Instances represents additional cue instance paths to unify with Path.
// Useful to unify data files into a component BuildPlan. Added in holos
// 0.101.7.
Instances []Instance `json:"instances,omitempty" yaml:"instances,omitempty"`
// WriteTo represents the holos render component --write-to flag. If empty,
// the default value for the --write-to flag is used.
WriteTo string `json:"writeTo,omitempty" yaml:"writeTo,omitempty"`
@@ -319,3 +323,27 @@ type Component struct {
// `cli.holos.run/description` to customize the log message of each BuildPlan.
Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"`
}
// Instance represents a data instance to unify with the configuration.
//
// Useful to unify json and yaml files with cue configuration files for
// integration with other tools. For example, executing holos render platform
// from a pull request workflow after [Kargo] executes the [yaml update] and
// [git wait for pr] promotion steps.
//
// [Kargo]: https://docs.kargo.io/
// [yaml update]: https://docs.kargo.io/references/promotion-steps#yaml-update
// [git wait for pr]: https://docs.kargo.io/references/promotion-steps#git-wait-for-pr
type Instance struct {
// Kind is a discriminator.
Kind string `json:"kind" yaml:"kind" cue:"\"ExtractYAML\""`
// Ignored unless kind is ExtractYAML.
ExtractYAML ExtractYAML `json:"extractYAML,omitempty" yaml:"extractYAML,omitempty"`
}
// ExtractYAML represents a cue data instance encoded as yaml or json. If Path
// refers to a directory all files in the directory are extracted
// non-recursively. Otherwise, path must refer to a file.
type ExtractYAML struct {
Path string `json:"path" yaml:"path"`
}

63
cmd/cmd.go Normal file
View File

@@ -0,0 +1,63 @@
package cmd
import (
"context"
"fmt"
"log/slog"
"os"
"runtime/pprof"
"runtime/trace"
"github.com/holos-run/holos/internal/cli"
"github.com/holos-run/holos/internal/holos"
)
// MakeMain makes a main function for the cli or tests.
func MakeMain(options ...holos.Option) func() int {
return func() (exitCode int) {
cfg := holos.New(options...)
slog.SetDefault(cfg.Logger())
ctx := context.Background()
if format := os.Getenv("HOLOS_CPU_PROFILE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
err := pprof.StartCPUProfile(f)
defer func() {
pprof.StopCPUProfile()
f.Close()
}()
if err != nil {
return cli.HandleError(ctx, err, cfg)
}
}
defer memProfile(ctx, cfg)
if format := os.Getenv("HOLOS_TRACE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
err := trace.Start(f)
defer func() {
trace.Stop()
f.Close()
}()
if err != nil {
return cli.HandleError(ctx, err, cfg)
}
}
feature := &holos.EnvFlagger{}
if err := cli.New(cfg, feature).ExecuteContext(ctx); err != nil {
return cli.HandleError(ctx, err, cfg)
}
return 0
}
}
func memProfile(ctx context.Context, cfg *holos.Config) {
if format := os.Getenv("HOLOS_MEM_PROFILE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
defer f.Close()
if err := pprof.WriteHeapProfile(f); err != nil {
_ = cli.HandleError(ctx, err, cfg)
}
}
}

View File

@@ -3,9 +3,9 @@ package main
import (
"os"
"github.com/holos-run/holos/internal/cli"
"github.com/holos-run/holos/cmd"
)
func main() {
os.Exit(cli.MakeMain()())
os.Exit(cmd.MakeMain()())
}

View File

@@ -6,13 +6,13 @@ import (
"testing"
cue "cuelang.org/go/cmd/cue/cmd"
"github.com/holos-run/holos/internal/cli"
"github.com/holos-run/holos/cmd"
"github.com/rogpeppe/go-internal/testscript"
)
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"holos": cli.MakeMain(),
"holos": cmd.MakeMain(),
"cue": cue.Main,
}))
}

View File

@@ -22,12 +22,14 @@ Package core contains schemas for a [Platform](<#Platform>) and [BuildPlan](<#Bu
- [type Chart](<#Chart>)
- [type Command](<#Command>)
- [type Component](<#Component>)
- [type ExtractYAML](<#ExtractYAML>)
- [type File](<#File>)
- [type FileContent](<#FileContent>)
- [type FileContentMap](<#FileContentMap>)
- [type FilePath](<#FilePath>)
- [type Generator](<#Generator>)
- [type Helm](<#Helm>)
- [type Instance](<#Instance>)
- [type InternalLabel](<#InternalLabel>)
- [type Join](<#Join>)
- [type Kind](<#Kind>)
@@ -169,6 +171,10 @@ type Component struct {
// Path represents the path of the component relative to the platform root.
// Injected as the tag variable "holos_component_path".
Path string `json:"path" yaml:"path"`
// Instances represents additional cue instance paths to unify with Path.
// Useful to unify data files into a component BuildPlan. Added in holos
// 0.101.7.
Instances []Instance `json:"instances,omitempty" yaml:"instances,omitempty"`
// WriteTo represents the holos render component --write-to flag. If empty,
// the default value for the --write-to flag is used.
WriteTo string `json:"writeTo,omitempty" yaml:"writeTo,omitempty"`
@@ -187,6 +193,17 @@ type Component struct {
}
```
<a name="ExtractYAML"></a>
## type ExtractYAML {#ExtractYAML}
ExtractYAML represents a cue data instance encoded as yaml or json. If Path refers to a directory all files in the directory are extracted non\-recursively. Otherwise, path must refer to a file.
```go
type ExtractYAML struct {
Path string `json:"path" yaml:"path"`
}
```
<a name="File"></a>
## type File {#File}
@@ -279,6 +296,22 @@ type Helm struct {
}
```
<a name="Instance"></a>
## type Instance {#Instance}
Instance represents a data instance to unify with the configuration.
Useful to unify json and yaml files with cue configuration files for integration with other tools. For example, executing holos render platform from a pull request workflow after [Kargo](<https://docs.kargo.io/>) executes the [yaml update](<https://docs.kargo.io/references/promotion-steps#yaml-update>) and [git wait for pr](<https://docs.kargo.io/references/promotion-steps#git-wait-for-pr>) promotion steps.
```go
type Instance struct {
// Kind is a discriminator.
Kind string `json:"kind" yaml:"kind" cue:"\"ExtractYAML\""`
// Ignored unless kind is ExtractYAML.
ExtractYAML ExtractYAML `json:"extractYAML,omitempty" yaml:"extractYAML,omitempty"`
}
```
<a name="InternalLabel"></a>
## type InternalLabel {#InternalLabel}

View File

@@ -0,0 +1,57 @@
---
description: Holos compared to other tools
sidebar_label: Comparison
slug: comparison
sidebar_position: 40
---
{/* cspell:ignore Prodan, rollouts */}
# Holos compared to other tools
## Timoni
Holos and Timoni both aim to solve similar problems but approach them at
different levels of the stack.
Timoni focuses on managing applications by evaluating [CUE] stored in OCI
containers. Its creator, Stephan Prodan, envisions a controller that applies the
resulting manifests. In this process, Timoni defers to [Flux] for managing Helm
charts within the cluster.
In contrast, Holos implements the [Rendered Manifests Pattern] and takes a
different approach, particularly in how it handles [Helm] charts. Like
[ArgoCD], Holos renders Helm charts into manifests using the `helm template`
command in its rendering pipeline. Holos differs from Timoni in several important
ways:
1. **Separation of Responsibilities:** Holos stops short of applying
rendered manifests to a cluster, leaving that task to existing tools like
[ArgoCD], [Flux], or even basic `kubectl apply` commands.
2. **Ecosystem Integration:** By focusing solely on rendering Kubernetes
manifests, Holos creates space for other tools to handle deployment and
management. For instance, Holos integrates seamlessly with [Kargo] for
progressive rollouts, as [Kargo] operates between Holos and the Kubernetes API.
This approach ensures that you're not locked into any specific tool and can
choose the best solution for each task.
3. **Platform Integration:** Holos focuses on integrating multiple Components
into a larger Platform. In Holos terminology, a Component refers to a wrapper
for [Helm] charts, [Kustomize] bases, or raw YAML files, integrated into the
rendering pipeline through [CUE]. A Platform represents the full combination of
these components.
4. **Explicit Rendering Pipeline:** Holos emphasizes flexibility in its
rendering pipeline. The system allows any tool that generates Kubernetes
manifests to be wrapped in a Generator, which can then feed into existing
transformers like [Kustomize]. This explicit separation makes Holos highly
adaptable for different workflows.
[Kargo]: https://kargo.io/
[Flux]: https://fluxcd.io
[Helm]: https://helm.sh
[ArgoCD]: https://argoproj.github.io/cd/
[Kustomize]: https://kustomize.io/
[CUE]: https://cuelang.org/
[Rendered Manifests Pattern]: https://akuity.io/blog/the-rendered-manifests-pattern

View File

@@ -14,3 +14,7 @@
/docs/local-cluster/ /docs/v1alpha5/topics/local-cluster/ 301
/docs/guides/helm /docs/v1alpha5/tutorial/helm-values/ 301
/docs/guides/helm/ /docs/v1alpha5/tutorial/helm-values/ 301
/docs/kargo /docs/v1alpha5/topics/kargo/ 301
/docs/kargo/ /docs/v1alpha5/topics/kargo/ 301
/docs/comparison /docs/v1alpha5/topics/comparison/ 301
/docs/comparison/ /docs/v1alpha5/topics/comparison/ 301

View File

@@ -5,17 +5,67 @@ import (
"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"
)
func LoadInstance(path string, tags []string) (*Instance, error) {
// 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)
@@ -26,20 +76,26 @@ func LoadInstance(path string, tags []string) (*Instance, error) {
ModuleRoot: root,
Tags: tags,
}
ctxt := cuecontext.New()
ctx := cuecontext.New()
instances := load.Instances([]string{leaf}, cfg)
values, err := ctx.BuildInstances(instances)
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: ctx,
ctx: ctxt,
cfg: cfg,
value: values[0],
value: value,
}
return inst, nil

View File

@@ -107,6 +107,20 @@ func (c *Component) Path() string {
return util.DotSlash(c.Component.Path)
}
// ExtractYAML returns the path values for the --extract-yaml command line flag.
func (c *Component) ExtractYAML() ([]string, error) {
if c == nil {
return nil, nil
}
instances := make([]string, 0, len(c.Component.Instances))
for _, instance := range c.Component.Instances {
if instance.Kind == "ExtractYAML" {
instances = append(instances, instance.ExtractYAML.Path)
}
}
return instances, nil
}
var _ holos.BuildPlan = &BuildPlan{}
var _ task = generatorTask{}
var _ task = transformersTask{}

View File

@@ -1,110 +1 @@
package cli
import (
"context"
"fmt"
"log/slog"
"os"
"runtime/pprof"
"runtime/trace"
"connectrpc.com/connect"
cue "cuelang.org/go/cue/errors"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)
func memProfile(ctx context.Context, cfg *holos.Config) {
if format := os.Getenv("HOLOS_MEM_PROFILE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
defer f.Close()
if err := pprof.WriteHeapProfile(f); err != nil {
_ = HandleError(ctx, err, cfg)
}
}
}
// MakeMain makes a main function for the cli or tests.
func MakeMain(options ...holos.Option) func() int {
return func() (exitCode int) {
cfg := holos.New(options...)
slog.SetDefault(cfg.Logger())
ctx := context.Background()
if format := os.Getenv("HOLOS_CPU_PROFILE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
err := pprof.StartCPUProfile(f)
defer func() {
pprof.StopCPUProfile()
f.Close()
}()
if err != nil {
return HandleError(ctx, err, cfg)
}
}
defer memProfile(ctx, cfg)
if format := os.Getenv("HOLOS_TRACE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
err := trace.Start(f)
defer func() {
trace.Stop()
f.Close()
}()
if err != nil {
return HandleError(ctx, err, cfg)
}
}
feature := &holos.EnvFlagger{}
if err := New(cfg, feature).ExecuteContext(ctx); err != nil {
return HandleError(ctx, err, cfg)
}
return 0
}
}
// HandleError is the top level error handler that unwraps and logs errors.
func HandleError(ctx context.Context, err error, hc *holos.Config) (exitCode int) {
// Connect errors have codes, log them.
log := hc.NewTopLevelLogger().With("code", connect.CodeOf(err))
var cueErr cue.Error
var errAt *errors.ErrorAt
if errors.As(err, &errAt) {
loc := errAt.Source.Loc()
err2 := errAt.Unwrap()
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s at %s", err2, loc), "err", err2, "loc", loc)
} else {
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s", err), "err", err)
}
// cue errors are bundled up as a list and refer to multiple files / lines.
if errors.As(err, &cueErr) {
msg := cue.Details(cueErr, nil)
if _, err := fmt.Fprint(hc.Stderr(), msg); err != nil {
log.ErrorContext(ctx, "could not write CUE error details: "+err.Error(), "err", err)
}
}
// connect errors have details and codes.
// Refer to https://connectrpc.com/docs/go/errors
if connectErr := new(connect.Error); errors.As(err, &connectErr) {
for _, detail := range connectErr.Details() {
msg, valueErr := detail.Value()
if valueErr != nil {
log.WarnContext(ctx, "could not decode error detail", "err", err, "type", detail.Type(), "note", "this usually means we don't have the schema for the protobuf message type")
continue
}
if info, ok := msg.(*errdetails.ErrorInfo); ok {
logDetail := log.With("reason", info.GetReason(), "domain", info.GetDomain())
for k, v := range info.GetMetadata() {
logDetail = logDetail.With(k, v)
}
logDetail.ErrorContext(ctx, info.String())
}
}
}
return 1
}

View File

@@ -43,6 +43,8 @@ func newPlatform(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd.Flags().IntVar(&concurrency, "concurrency", runtime.NumCPU(), "number of components to render concurrently")
var platform string
cmd.Flags().StringVar(&platform, "platform", "./platform", "platform directory path")
var extractYAMLs holos.StringSlice
cmd.Flags().Var(&extractYAMLs, "extract-yaml", "data file paths to extract and unify with the platform config")
var selector holos.Selector
cmd.Flags().VarP(&selector, "selector", "l", "label selector (e.g. label==string,label!=string)")
tagMap := make(holos.TagMap)
@@ -57,7 +59,7 @@ func newPlatform(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
log.WarnContext(ctx, fmt.Sprintf(msg, platform))
}
inst, err := builder.LoadInstance(platform, tagMap.Tags())
inst, err := builder.LoadInstance(platform, extractYAMLs, tagMap.Tags())
if err != nil {
return errors.Wrap(err)
}
@@ -107,12 +109,14 @@ func newComponent(cfg *holos.Config, feature holos.Flagger) *cobra.Command {
cmd.Flags().VarP(&tagMap, "inject", "t", tagHelp)
var concurrency int
cmd.Flags().IntVar(&concurrency, "concurrency", runtime.NumCPU(), "number of concurrent build steps")
var extractYAMLs holos.StringSlice
cmd.Flags().Var(&extractYAMLs, "extract-yaml", "data file paths to extract and unify with the platform config")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx := cmd.Root().Context()
path := args[0]
inst, err := builder.LoadInstance(path, tagMap.Tags())
inst, err := builder.LoadInstance(path, extractYAMLs, tagMap.Tags())
if err != nil {
return errors.Wrap(err)
}
@@ -146,7 +150,11 @@ func makeComponentRenderFunc(w io.Writer, prefixArgs, cliTags []string) func(con
if err != nil {
return errors.Wrap(err)
}
args := make([]string, 0, 10+len(prefixArgs)+(len(tags)*2))
filepaths, err := component.ExtractYAML()
if err != nil {
return errors.Wrap(err)
}
args := make([]string, 0, 10+len(prefixArgs)+(len(tags)*2+len(filepaths)*2))
args = append(args, prefixArgs...)
args = append(args, "render", "component")
for _, tag := range cliTags {
@@ -155,6 +163,9 @@ func makeComponentRenderFunc(w io.Writer, prefixArgs, cliTags []string) func(con
for _, tag := range tags {
args = append(args, "--inject", tag)
}
for _, path := range filepaths {
args = append(args, "--extract-yaml", path)
}
args = append(args, component.Path())
if _, err := util.RunCmdA(ctx, w, "holos", args...); err != nil {
return errors.Format("could not render component: %w", err)

View File

@@ -1,14 +1,18 @@
package cli
import (
"context"
_ "embed"
"fmt"
"log/slog"
"connectrpc.com/connect"
"github.com/spf13/cobra"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"github.com/holos-run/holos/version"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/internal/server"
@@ -28,7 +32,8 @@ import (
"github.com/holos-run/holos/internal/cli/token"
"github.com/holos-run/holos/internal/cli/txtar"
cue "cuelang.org/go/cmd/cue/cmd"
cueCmd "cuelang.org/go/cmd/cue/cmd"
cue_errors "cuelang.org/go/cue/errors"
)
//go:embed help.txt
@@ -119,7 +124,7 @@ func newOrgCmd(feature holos.Flagger) (cmd *cobra.Command) {
func newCueCmd() (cmd *cobra.Command) {
// Get a handle on the cue root command fields.
root, _ := cue.New([]string{})
root, _ := cueCmd.New([]string{})
// Copy the fields to our embedded command.
cmd = command.New("cue")
cmd.Short = root.Short
@@ -130,8 +135,52 @@ func newCueCmd() (cmd *cobra.Command) {
// We do it this way so we handle errors correctly.
cmd.RunE = func(cmd *cobra.Command, args []string) error {
cueRootCommand, _ := cue.New(args)
cueRootCommand, _ := cueCmd.New(args)
return cueRootCommand.Run(cmd.Root().Context())
}
return cmd
}
// HandleError is the top level error handler that unwraps and logs errors.
func HandleError(ctx context.Context, err error, hc *holos.Config) (exitCode int) {
// Connect errors have codes, log them.
log := hc.NewTopLevelLogger().With("code", connect.CodeOf(err))
var cueErr cue_errors.Error
var errAt *errors.ErrorAt
if errors.As(err, &errAt) {
loc := errAt.Source.Loc()
err2 := errAt.Unwrap()
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s at %s", err2, loc), "err", err2, "loc", loc)
} else {
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s", err), "err", err)
}
// cue errors are bundled up as a list and refer to multiple files / lines.
if errors.As(err, &cueErr) {
msg := cue_errors.Details(cueErr, nil)
if _, err := fmt.Fprint(hc.Stderr(), msg); err != nil {
log.ErrorContext(ctx, "could not write CUE error details: "+err.Error(), "err", err)
}
}
// connect errors have details and codes.
// Refer to https://connectrpc.com/docs/go/errors
if connectErr := new(connect.Error); errors.As(err, &connectErr) {
for _, detail := range connectErr.Details() {
msg, valueErr := detail.Value()
if valueErr != nil {
log.WarnContext(ctx, "could not decode error detail", "err", err, "type", detail.Type(), "note", "this usually means we don't have the schema for the protobuf message type")
continue
}
if info, ok := msg.(*errdetails.ErrorInfo); ok {
logDetail := log.With("reason", info.GetReason(), "domain", info.GetDomain())
for k, v := range info.GetMetadata() {
logDetail = logDetail.With(k, v)
}
logDetail.ErrorContext(ctx, info.String())
}
}
}
return 1
}

View File

@@ -30,13 +30,15 @@ func newShowPlatformCmd() (cmd *cobra.Command) {
var platform string
cmd.Flags().StringVar(&platform, "platform", "./platform", "platform directory path")
var extractYAMLs holos.StringSlice
cmd.Flags().Var(&extractYAMLs, "extract-yaml", "data file paths to extract and unify with the platform config")
var format string
cmd.Flags().StringVar(&format, "format", "yaml", "yaml or json format")
tagMap := make(holos.TagMap)
cmd.Flags().VarP(&tagMap, "inject", "t", "set the value of a cue @tag field from a key=value pair")
cmd.RunE = func(c *cobra.Command, args []string) (err error) {
inst, err := builder.LoadInstance(platform, tagMap.Tags())
inst, err := builder.LoadInstance(platform, extractYAMLs, tagMap.Tags())
if err != nil {
return errors.Wrap(err)
}
@@ -64,6 +66,8 @@ func newShowBuildPlanCmd() (cmd *cobra.Command) {
var platform string
cmd.Flags().StringVar(&platform, "platform", "./platform", "platform directory path")
var extractYAMLs holos.StringSlice
cmd.Flags().Var(&extractYAMLs, "extract-yaml", "data file paths to extract and unify with the platform config")
var format string
cmd.Flags().StringVar(&format, "format", "yaml", "yaml or json format")
var selector holos.Selector
@@ -75,7 +79,7 @@ func newShowBuildPlanCmd() (cmd *cobra.Command) {
cmd.RunE = func(c *cobra.Command, args []string) (err error) {
path := platform
inst, err := builder.LoadInstance(path, tagMap.Tags())
inst, err := builder.LoadInstance(path, extractYAMLs, tagMap.Tags())
if err != nil {
return errors.Wrap(err)
}
@@ -122,7 +126,11 @@ func makeBuildFunc(encoder holos.OrderedEncoder, opts holos.BuildOpts) func(cont
return errors.Wrap(err)
}
tags = append(tags, opts.Tags...)
inst, err := builder.LoadInstance(component.Path(), tags)
filepaths, err := component.ExtractYAML()
if err != nil {
return errors.Wrap(err)
}
inst, err := builder.LoadInstance(component.Path(), filepaths, tags)
if err != nil {
return errors.Wrap(err)
}

View File

@@ -334,6 +334,11 @@ package core
// Injected as the tag variable "holos_component_path".
path: string @go(Path)
// Instances represents additional cue instance paths to unify with Path.
// Useful to unify data files into a component BuildPlan. Added in holos
// 0.101.7.
instances?: [...#Instance] @go(Instances,[]Instance)
// WriteTo represents the holos render component --write-to flag. If empty,
// the default value for the --write-to flag is used.
writeTo?: string @go(WriteTo)
@@ -353,3 +358,28 @@ package core
// `cli.holos.run/description` to customize the log message of each BuildPlan.
annotations?: {[string]: string} @go(Annotations,map[string]string)
}
// Instance represents a data instance to unify with the configuration.
//
// Useful to unify json and yaml files with cue configuration files for
// integration with other tools. For example, executing holos render platform
// from a pull request workflow after [Kargo] executes the [yaml update] and
// [git wait for pr] promotion steps.
//
// [Kargo]: https://docs.kargo.io/
// [yaml update]: https://docs.kargo.io/references/promotion-steps#yaml-update
// [git wait for pr]: https://docs.kargo.io/references/promotion-steps#git-wait-for-pr
#Instance: {
// Kind is a discriminator.
kind: string & "ExtractYAML" @go(Kind)
// Ignored unless kind is ExtractYAML.
extractYAML?: #ExtractYAML @go(ExtractYAML)
}
// ExtractYAML represents a cue data instance encoded as yaml or json. If Path
// refers to a directory all files in the directory are extracted
// non-recursively. Otherwise, path must refer to a file.
#ExtractYAML: {
path: string @go(Path)
}

View File

@@ -1,6 +1,6 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f projects/argocd/components/kargo/vendor/1.0.3/kargo/resources/crds/kargo.akuity.io_freights.yaml
//timoni:generate timoni vendor crd -f /Users/jeff/Holos/kargo-demo/deploy/components/kargo/kargo.gen.yaml
package v1alpha1

View File

@@ -1,6 +1,6 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f projects/argocd/components/kargo/vendor/1.0.3/kargo/resources/crds/kargo.akuity.io_projects.yaml
//timoni:generate timoni vendor crd -f /Users/jeff/Holos/kargo-demo/deploy/components/kargo/kargo.gen.yaml
package v1alpha1

View File

@@ -1,6 +1,6 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f projects/argocd/components/kargo/vendor/1.0.3/kargo/resources/crds/kargo.akuity.io_promotions.yaml
//timoni:generate timoni vendor crd -f /Users/jeff/Holos/kargo-demo/deploy/components/kargo/kargo.gen.yaml
package v1alpha1
@@ -74,10 +74,90 @@ import "strings"
// As is the alias this step can be referred to as.
as?: string
// Config is the configuration for the directive.
// Config is opaque configuration for the PromotionStep that is
// understood
// only by each PromotionStep's implementation. It is legal to
// utilize
// expressions in defining values at any level of this block.
// See https://docs.kargo.io/references/expression-language for
// details.
config?: _
// Retry is the retry policy for this step.
retry?: {
// ErrorThreshold is the number of consecutive times the step must
// fail (for
// any reason) before retries are abandoned and the entire
// Promotion is marked
// as failed.
//
// If this field is set to 0, the effective default will be a
// step-specific
// one. If no step-specific default exists (i.e. is also 0), the
// effective
// default will be the system-wide default of 1.
//
// A value of 1 will cause the Promotion to be marked as failed
// after just
// a single failure; i.e. no retries will be attempted.
//
// There is no option to specify an infinite number of retries
// using a value
// such as -1.
//
// In a future release, Kargo is likely to become capable of
// distinguishing
// between recoverable and non-recoverable step failures. At that
// time, it is
// planned that unrecoverable failures will not be subject to this
// threshold
// and will immediately cause the Promotion to be marked as failed
// without
// further condition.
errorThreshold?: int
// Timeout is the soft maximum interval in which a step that
// returns a Running
// status (which typically indicates it's waiting for something to
// happen)
// may be retried.
//
// The maximum is a soft one because the check for whether the
// interval has
// elapsed occurs AFTER the step has run. This effectively means a
// step may
// run ONCE beyond the close of the interval.
//
// If this field is set to nil, the effective default will be a
// step-specific
// one. If no step-specific default exists (i.e. is also nil), the
// effective
// default will be the system-wide default of 0.
//
// A value of 0 will cause the step to be retried indefinitely
// unless the
// ErrorThreshold is reached.
timeout?: string
}
// Uses identifies a runner that can execute this step.
uses: strings.MinRunes(1)
}]
// Vars is a list of variables that can be referenced by
// expressions in
// promotion steps.
vars?: [...{
// Name is the name of the variable.
name: strings.MinRunes(1) & {
=~"^[a-zA-Z_]\\w*$"
}
// Value is the value of the variable. It is allowed to utilize
// expressions
// in the value.
// See https://docs.kargo.io/references/expression-language for
// details.
value: string
}]
}

View File

@@ -1,6 +1,6 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f projects/argocd/components/kargo/vendor/1.0.3/kargo/resources/crds/kargo.akuity.io_stages.yaml
//timoni:generate timoni vendor crd -f /Users/jeff/Holos/kargo-demo/deploy/components/kargo/kargo.gen.yaml
package v1alpha1
@@ -52,6 +52,11 @@ import "strings"
// Freight into the Stage.
#StageSpec: {
promotionTemplate?: {
// PromotionTemplateSpec describes the (partial) specification of
// a Promotion
// for a Stage. This is a template that can be used to create a
// Promotion for a
// Stage.
spec: {
// Steps specifies the directives to be executed as part of a
// Promotion.
@@ -62,12 +67,92 @@ import "strings"
// As is the alias this step can be referred to as.
as?: string
// Config is the configuration for the directive.
// Config is opaque configuration for the PromotionStep that is
// understood
// only by each PromotionStep's implementation. It is legal to
// utilize
// expressions in defining values at any level of this block.
// See https://docs.kargo.io/references/expression-language for
// details.
config?: _
// Retry is the retry policy for this step.
retry?: {
// ErrorThreshold is the number of consecutive times the step must
// fail (for
// any reason) before retries are abandoned and the entire
// Promotion is marked
// as failed.
//
// If this field is set to 0, the effective default will be a
// step-specific
// one. If no step-specific default exists (i.e. is also 0), the
// effective
// default will be the system-wide default of 1.
//
// A value of 1 will cause the Promotion to be marked as failed
// after just
// a single failure; i.e. no retries will be attempted.
//
// There is no option to specify an infinite number of retries
// using a value
// such as -1.
//
// In a future release, Kargo is likely to become capable of
// distinguishing
// between recoverable and non-recoverable step failures. At that
// time, it is
// planned that unrecoverable failures will not be subject to this
// threshold
// and will immediately cause the Promotion to be marked as failed
// without
// further condition.
errorThreshold?: int
// Timeout is the soft maximum interval in which a step that
// returns a Running
// status (which typically indicates it's waiting for something to
// happen)
// may be retried.
//
// The maximum is a soft one because the check for whether the
// interval has
// elapsed occurs AFTER the step has run. This effectively means a
// step may
// run ONCE beyond the close of the interval.
//
// If this field is set to nil, the effective default will be a
// step-specific
// one. If no step-specific default exists (i.e. is also nil), the
// effective
// default will be the system-wide default of 0.
//
// A value of 0 will cause the step to be retried indefinitely
// unless the
// ErrorThreshold is reached.
timeout?: string
}
// Uses identifies a runner that can execute this step.
uses: strings.MinRunes(1)
}] & [_, ...]
// Vars is a list of variables that can be referenced by
// expressions in
// promotion steps.
vars?: [...{
// Name is the name of the variable.
name: strings.MinRunes(1) & {
=~"^[a-zA-Z_]\\w*$"
}
// Value is the value of the variable. It is allowed to utilize
// expressions
// in the value.
// See https://docs.kargo.io/references/expression-language for
// details.
value: string
}]
}
}

View File

@@ -1,6 +1,6 @@
// Code generated by timoni. DO NOT EDIT.
//timoni:generate timoni vendor crd -f projects/argocd/components/kargo/vendor/1.0.3/kargo/resources/crds/kargo.akuity.io_warehouses.yaml
//timoni:generate timoni vendor crd -f /Users/jeff/Holos/kargo-demo/deploy/components/kargo/kargo.gen.yaml
package v1alpha1
@@ -52,6 +52,7 @@ import "strings"
// This field is optional. When left unspecified, the field is
// implicitly
// treated as if its value were "Automatic".
// Accepted values: Automatic, Manual
freightCreationPolicy?: "Automatic" | "Manual" | *"Automatic"
// Interval is the reconciliation interval for this Warehouse. On
@@ -170,6 +171,7 @@ import "strings"
// field is optional. When left unspecified, the field is
// implicitly treated
// as if its value were "NewestFromBranch".
// Accepted values: Lexical, NewestFromBranch, NewestTag, SemVer
commitSelectionStrategy?: "Lexical" | "NewestFromBranch" | "NewestTag" | "SemVer" | *"NewestFromBranch"
// DiscoveryLimit is an optional limit on the number of commits
@@ -324,6 +326,7 @@ import "strings"
// left unspecified, the field is implicitly treated as if its
// value were
// "SemVer".
// Accepted values: Digest, Lexical, NewestBuild, SemVer
imageSelectionStrategy?: "Digest" | "Lexical" | "NewestBuild" | "SemVer" | *"SemVer"
// InsecureSkipTLSVerify specifies whether certificate

View File

@@ -21,6 +21,8 @@ type Platform interface {
type Component interface {
Describe() string
Path() string
// ExtractYAML represents the values of the --extract-yaml flag
ExtractYAML() ([]string, error)
Tags() ([]string, error)
WriteTo() string
Labels() Labels

View File

@@ -42,6 +42,9 @@ func FindCueMod(path string) (root string, err error) {
return root, nil
}
// FindRootLeaf returns the root path containing the cue.mod and the leaf path
// relative to the root for the given target path. FindRootLeaf calls
// [filepath.Clean] on the returned paths.
func FindRootLeaf(target string) (root string, leaf string, err error) {
if root, err = FindCueMod(target); err != nil {
return "", "", err

View File

@@ -4,5 +4,5 @@ deps:
- remote: buf.build
owner: bufbuild
repository: protovalidate
commit: 5a7b106cbb87462d9a8c9ffecdbd2e38
digest: shake256:2f7efa5a904668219f039d4f6eeb51e871f8f7f5966055a10663cba335bd65f76cac84da3fa758ab7b5dcb489ec599521390ce3951d119fb56df1fc2def16bb0
commit: a3320276596649bcad929ac829d451f4
digest: shake256:a6e5f64fd3fd47e3e8568e9753f59a1566f56c11ec055baf65463d3bca3499f6f16c2d6f5628fa41cfd0f4fa7e72abe65be4efd77d269749492472ed4cc4070d

View File

@@ -1 +1 @@
6
8