mirror of
https://github.com/holos-run/holos.git
synced 2026-03-19 00:37:45 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c76061b0d | ||
|
|
f60db8fa1f |
9
cmd/holos/testdata/issue15_cue_errors.txt
vendored
9
cmd/holos/testdata/issue15_cue_errors.txt
vendored
@@ -1,7 +1,7 @@
|
||||
# Want cue errors to show files and lines
|
||||
! exec holos build .
|
||||
stderr 'could not decode: content: cannot convert non-concrete value string'
|
||||
stderr '/component.cue:6:1$'
|
||||
stderr '^apiObjectMap.foo.bar: cannot convert non-concrete value string'
|
||||
stderr '/component.cue:7:20$'
|
||||
|
||||
-- cue.mod --
|
||||
package holos
|
||||
@@ -11,5 +11,6 @@ package holos
|
||||
apiVersion: "holos.run/v1alpha1"
|
||||
kind: "KubernetesObjects"
|
||||
cluster: string @tag(cluster, string)
|
||||
content: foo
|
||||
foo: string
|
||||
|
||||
apiObjectMap: foo: bar: baz
|
||||
baz: string
|
||||
|
||||
57
cmd/holos/testdata/issue25_apiobjects_cue.txt
vendored
Normal file
57
cmd/holos/testdata/issue25_apiobjects_cue.txt
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
# Want kube api objects in the apiObjects output.
|
||||
exec holos build .
|
||||
stdout '^kind: SecretStore$'
|
||||
stdout '# Source: CUE apiObjects.SecretStore.default'
|
||||
|
||||
-- cue.mod --
|
||||
package holos
|
||||
-- component.cue --
|
||||
package holos
|
||||
|
||||
apiVersion: "holos.run/v1alpha1"
|
||||
kind: "KubernetesObjects"
|
||||
cluster: string @tag(cluster, string)
|
||||
|
||||
#SecretStore: {
|
||||
kind: string
|
||||
metadata: name: string
|
||||
}
|
||||
|
||||
#APIObjects & {
|
||||
apiObjects: {
|
||||
SecretStore: {
|
||||
default: #SecretStore & { metadata: name: "default" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-- schema.cue --
|
||||
package holos
|
||||
|
||||
// #APIObjects is the output type for api objects produced by cue. A map is used to aid debugging and clarity.
|
||||
import "encoding/yaml"
|
||||
|
||||
#APIObjects: {
|
||||
// apiObjects holds each the api objects produced by cue.
|
||||
apiObjects: {
|
||||
[Kind=_]: {
|
||||
[Name=_]: {
|
||||
kind: Kind
|
||||
metadata: name: Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apiObjectsContent holds the marshalled representation of apiObjects
|
||||
apiObjectMap: {
|
||||
for kind, v in apiObjects {
|
||||
"\(kind)": {
|
||||
for name, obj in v {
|
||||
"\(name)": yaml.Marshal(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
cmd/holos/testdata/issue25_apiobjects_helm.txt
vendored
Normal file
58
cmd/holos/testdata/issue25_apiobjects_helm.txt
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
# Want kube api objects in the apiObjects output.
|
||||
exec holos build .
|
||||
stdout '^kind: SecretStore$'
|
||||
stdout '# Source: CUE apiObjects.SecretStore.default'
|
||||
stderr 'skipping helm: no chart name specified'
|
||||
|
||||
-- cue.mod --
|
||||
package holos
|
||||
-- component.cue --
|
||||
package holos
|
||||
|
||||
apiVersion: "holos.run/v1alpha1"
|
||||
kind: "HelmChart"
|
||||
cluster: string @tag(cluster, string)
|
||||
|
||||
#SecretStore: {
|
||||
kind: string
|
||||
metadata: name: string
|
||||
}
|
||||
|
||||
#APIObjects & {
|
||||
apiObjects: {
|
||||
SecretStore: {
|
||||
default: #SecretStore & { metadata: name: "default" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-- schema.cue --
|
||||
package holos
|
||||
|
||||
// #APIObjects is the output type for api objects produced by cue. A map is used to aid debugging and clarity.
|
||||
import "encoding/yaml"
|
||||
|
||||
#APIObjects: {
|
||||
// apiObjects holds each the api objects produced by cue.
|
||||
apiObjects: {
|
||||
[Kind=_]: {
|
||||
[Name=_]: {
|
||||
kind: Kind
|
||||
metadata: name: Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apiObjectsContent holds the marshalled representation of apiObjects
|
||||
apiObjectMap: {
|
||||
for kind, v in apiObjects {
|
||||
"\(kind)": {
|
||||
for name, obj in v {
|
||||
"\(name)": yaml.Marshal(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
cmd/holos/testdata/issue25_show_object_names.txt
vendored
Normal file
22
cmd/holos/testdata/issue25_show_object_names.txt
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Want api object kind and name in errors
|
||||
! exec holos build .
|
||||
stderr 'apiObjects.secretstore.default.foo: field not allowed'
|
||||
|
||||
-- cue.mod --
|
||||
package holos
|
||||
-- component.cue --
|
||||
package holos
|
||||
|
||||
apiVersion: "holos.run/v1alpha1"
|
||||
kind: "KubernetesObjects"
|
||||
cluster: string @tag(cluster, string)
|
||||
|
||||
#SecretStore: {
|
||||
metadata: name: string
|
||||
}
|
||||
|
||||
apiObjects: {
|
||||
secretstore: {
|
||||
default: #SecretStore & { foo: "not allowed" }
|
||||
}
|
||||
}
|
||||
@@ -8,21 +8,24 @@ package holos
|
||||
// - Namespace
|
||||
// - ServiceAccount eso-reader, eso-writer
|
||||
|
||||
import "list"
|
||||
|
||||
// objects are kubernetes api objects to apply.
|
||||
objects: list.FlattenN(_objects, 1)
|
||||
|
||||
_objects: [
|
||||
#CredsRefresherIAM.role,
|
||||
#CredsRefresherIAM.binding,
|
||||
for ns in #PlatformNamespaces {(#PlatformNamespaceObjects & {_ns: ns}).objects},
|
||||
]
|
||||
|
||||
// No flux kustomization
|
||||
ksObjects: []
|
||||
|
||||
{} & #KubernetesObjects
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
let role = #CredsRefresherIAM.role
|
||||
let binding = #CredsRefresherIAM.binding
|
||||
ClusterRole: "\(role.metadata.name)": role
|
||||
ClusterRoleBinding: "\(binding.metadata.name)": binding
|
||||
for ns in #PlatformNamespaces {
|
||||
for obj in (#PlatformNamespaceObjects & {_ns: ns}).objects {
|
||||
let Kind = obj.kind
|
||||
let Name = obj.metadata.name
|
||||
"\(Kind)": "\(ns.name)/\(Name)": obj
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#InputKeys: {
|
||||
cluster: "provisioner"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package holos
|
||||
|
||||
import "list"
|
||||
|
||||
#TargetNamespace: "default"
|
||||
|
||||
#InputKeys: {
|
||||
@@ -20,12 +18,14 @@ import "list"
|
||||
]
|
||||
}
|
||||
|
||||
objects: list.FlattenN(_objects, 1)
|
||||
|
||||
_objects: [
|
||||
for ns in #PlatformNamespaces {
|
||||
(#PlatformNamespaceObjects & {_ns: ns}).objects
|
||||
},
|
||||
]
|
||||
|
||||
{} & #KubernetesObjects
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
for ns in #PlatformNamespaces {
|
||||
for obj in (#PlatformNamespaceObjects & {_ns: ns}).objects {
|
||||
let Kind = obj.kind
|
||||
let Name = obj.metadata.name
|
||||
"\(Kind)": "\(Name)": obj
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,16 @@ package holos
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// objects are kubernetes api objects to apply
|
||||
objects: #CredsRefresherService.objects
|
||||
|
||||
// output kubernetes api objects for holos
|
||||
{} & #KubernetesObjects
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
for obj in #CredsRefresherService.objects {
|
||||
let Kind = obj.kind
|
||||
let Name = obj.metadata.name
|
||||
"\(Kind)": "\(Name)": obj
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#InputKeys: {
|
||||
project: "secrets"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package holos
|
||||
|
||||
import "list"
|
||||
|
||||
#TargetNamespace: "default"
|
||||
|
||||
#InputKeys: {
|
||||
@@ -18,12 +16,14 @@ import "list"
|
||||
]
|
||||
}
|
||||
|
||||
objects: list.FlattenN(_objects, 1)
|
||||
|
||||
_objects: [
|
||||
for ns in #PlatformNamespaces {
|
||||
(#PlatformNamespaceObjects & {_ns: ns}).objects
|
||||
},
|
||||
]
|
||||
|
||||
{} & #KubernetesObjects
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
for ns in #PlatformNamespaces {
|
||||
for obj in (#PlatformNamespaceObjects & {_ns: ns}).objects {
|
||||
let Kind = obj.kind
|
||||
let Name = obj.metadata.name
|
||||
"\(Kind)": "\(Name)": obj
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,13 @@ package holos
|
||||
|
||||
#Kustomization: spec: dependsOn: [{name: #InstancePrefix + "-eso"}]
|
||||
|
||||
objects: [
|
||||
#SecretStore,
|
||||
#ExternalSecret & {_name: "validate"},
|
||||
]
|
||||
|
||||
{} & #KubernetesObjects
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
SecretStore: default: #SecretStore
|
||||
|
||||
ExternalSecret: validate: #ExternalSecret & {
|
||||
_name: "validate"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,11 @@ package holos
|
||||
url: "https://ceph.github.io/csi-charts"
|
||||
}
|
||||
}
|
||||
objects: [
|
||||
#SecretStore,
|
||||
#ExternalSecret & {_name: #SecretName},
|
||||
]
|
||||
|
||||
apiObjects: {
|
||||
SecretStore: default: #SecretStore
|
||||
ExternalSecret: "\(#SecretName)": #ExternalSecret & {
|
||||
_name: #SecretName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,13 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
|
||||
// #CommonLabels are mixed into every kubernetes api object.
|
||||
#CommonLabels: {
|
||||
"holos.run/stage.name": #InputKeys.stage
|
||||
"holos.run/project.name": #InputKeys.project
|
||||
"holos.run/component.name": #InputKeys.component
|
||||
"holos.run/stage.name": #InputKeys.stage
|
||||
"holos.run/project.name": #InputKeys.project
|
||||
"holos.run/component.name": #InputKeys.component
|
||||
"app.kubernetes.io/part-of": #InputKeys.stage
|
||||
"app.kubernetes.io/name": #InputKeys.project
|
||||
"app.kubernetes.io/component": #InputKeys.component
|
||||
"app.kubernetes.io/instance": #InstanceName
|
||||
...
|
||||
}
|
||||
|
||||
@@ -178,6 +182,29 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
}
|
||||
}
|
||||
|
||||
// #APIObjects is the output type for api objects produced by cue. A map is used to aid debugging and clarity.
|
||||
#APIObjects: {
|
||||
// apiObjects holds each the api objects produced by cue.
|
||||
apiObjects: {
|
||||
[Kind=_]: {
|
||||
[Name=_]: metav1.#TypeMeta & {
|
||||
kind: Kind
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apiObjectsContent holds the marshalled representation of apiObjects
|
||||
apiObjectMap: {
|
||||
for kind, v in apiObjects {
|
||||
"\(kind)": {
|
||||
for name, obj in v {
|
||||
"\(name)": yaml.Marshal(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #OutputTypeMeta is shared among all output types
|
||||
#OutputTypeMeta: {
|
||||
// apiVersion is the output api version
|
||||
@@ -197,14 +224,10 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
// #KubernetesObjectOutput is the output schema of a single component.
|
||||
#KubernetesObjects: {
|
||||
#OutputTypeMeta
|
||||
#APIObjects
|
||||
|
||||
// kind KubernetesObjects provides a yaml text stream of kubernetes api objects in the out field.
|
||||
kind: "KubernetesObjects"
|
||||
// objects holds a list of the kubernetes api objects to configure.
|
||||
objects: [...metav1.#TypeMeta] | *[]
|
||||
// content holds the rendered yaml text stream of kubernetes api objects.
|
||||
content: yaml.MarshalStream(objects)
|
||||
contentType: "application/yaml"
|
||||
// ksObjects holds the flux Kustomization objects for gitops
|
||||
ksObjects: [...#Kustomization] | *[#Kustomization]
|
||||
// ksContent is the yaml representation of kustomization
|
||||
@@ -213,6 +236,8 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
platform: #Platform
|
||||
}
|
||||
|
||||
objects: "not allowed"
|
||||
|
||||
// #Chart defines an upstream helm chart
|
||||
#Chart: {
|
||||
name: string
|
||||
@@ -229,6 +254,8 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
// #HelmChart is a holos component which produces kubernetes api objects from cue values provided to the helm template command.
|
||||
#HelmChart: {
|
||||
#OutputTypeMeta
|
||||
#APIObjects
|
||||
|
||||
kind: "HelmChart"
|
||||
// ksObjects holds the flux Kustomization objects for gitops.
|
||||
ksObjects: [...#Kustomization] | *[#Kustomization]
|
||||
@@ -246,11 +273,6 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
platform: #Platform
|
||||
// instance returns the key values of the holos component instance.
|
||||
instance: #InputKeys
|
||||
// objects holds a list of the kubernetes api objects to configure.
|
||||
objects: [...metav1.#TypeMeta] | *[]
|
||||
// content holds the rendered yaml text stream of kubernetes api objects.
|
||||
content: yaml.MarshalStream(objects)
|
||||
contentType: "application/yaml"
|
||||
}
|
||||
|
||||
// #PlatformSpec is the output schema of a platform specification.
|
||||
|
||||
@@ -20,7 +20,7 @@ func makeBuildRunFunc(cfg *holos.Config) command.RunFunc {
|
||||
}
|
||||
outs := make([]string, 0, len(results))
|
||||
for _, result := range results {
|
||||
outs = append(outs, result.Content)
|
||||
outs = append(outs, result.FinalOutput())
|
||||
}
|
||||
out := strings.Join(outs, "---\n")
|
||||
if _, err := fmt.Fprintln(cmd.OutOrStdout(), out); err != nil {
|
||||
|
||||
@@ -29,7 +29,7 @@ func makeRenderRunFunc(cfg *holos.Config) command.RunFunc {
|
||||
for _, result := range results {
|
||||
// API Objects
|
||||
path := result.Filename(cfg.WriteTo(), cfg.ClusterName())
|
||||
if err := result.Save(ctx, path, result.Content); err != nil {
|
||||
if err := result.Save(ctx, path, result.FinalOutput()); err != nil {
|
||||
return wrapper.Wrap(err)
|
||||
}
|
||||
// Kustomization
|
||||
|
||||
@@ -12,9 +12,11 @@ import (
|
||||
"github.com/holos-run/holos/pkg/logger"
|
||||
"github.com/holos-run/holos/pkg/util"
|
||||
"github.com/holos-run/holos/pkg/wrapper"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
"cuelang.org/go/cue/load"
|
||||
@@ -73,12 +75,16 @@ type Metadata struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// apiObjectMap is the shape of marshalled api objects returned from cue to the
|
||||
// holos cli. A map is used to improve the clarity of error messages from cue.
|
||||
type apiObjectMap map[string]map[string]string
|
||||
|
||||
// Result is the build result for display or writing.
|
||||
type Result struct {
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ContentType string `json:"contentType"`
|
||||
KsContent string `json:"ksContent,omitempty"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
KsContent string `json:"ksContent,omitempty"`
|
||||
APIObjectMap apiObjectMap `json:"apiObjectMap,omitempty"`
|
||||
finalOutput string
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
@@ -94,15 +100,14 @@ type Chart struct {
|
||||
|
||||
// A HelmChart represents a helm command to provide chart values in order to render kubernetes api objects.
|
||||
type HelmChart struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string `json:"kind"`
|
||||
Metadata Metadata `json:"metadata"`
|
||||
KsContent string `json:"ksContent"`
|
||||
Namespace string `json:"namespace"`
|
||||
Chart Chart `json:"chart"`
|
||||
ValuesContent string `json:"valuesContent"`
|
||||
ContentType string `json:"contentType"`
|
||||
Content string `json:"content"`
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Kind string `json:"kind"`
|
||||
Metadata Metadata `json:"metadata"`
|
||||
KsContent string `json:"ksContent"`
|
||||
Namespace string `json:"namespace"`
|
||||
Chart Chart `json:"chart"`
|
||||
ValuesContent string `json:"valuesContent"`
|
||||
APIObjectMap apiObjectMap `json:"APIObjectMap"`
|
||||
}
|
||||
|
||||
// Name returns the metadata name of the result. Equivalent to the
|
||||
@@ -119,6 +124,42 @@ func (r *Result) KustomizationFilename(writeTo string, cluster string) string {
|
||||
return filepath.Join(writeTo, "clusters", cluster, "holos", "components", r.Name()+"-kustomization.gen.yaml")
|
||||
}
|
||||
|
||||
// FinalOutput returns the final rendered output.
|
||||
func (r *Result) FinalOutput() string {
|
||||
return r.finalOutput
|
||||
}
|
||||
|
||||
// addAPIObjects adds the overlay api objects to finalOutput.
|
||||
func (r *Result) addOverlayObjects(log *slog.Logger) {
|
||||
b := []byte(r.FinalOutput())
|
||||
kinds := make([]string, 0, len(r.APIObjectMap))
|
||||
// Sort the keys
|
||||
for kind := range r.APIObjectMap {
|
||||
kinds = append(kinds, kind)
|
||||
}
|
||||
slices.Sort(kinds)
|
||||
|
||||
for _, kind := range kinds {
|
||||
v := r.APIObjectMap[kind]
|
||||
// Sort the keys
|
||||
names := make([]string, 0, len(v))
|
||||
for name := range v {
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.Sort(names)
|
||||
|
||||
for _, name := range names {
|
||||
yamlString := v[name]
|
||||
log.Debug(fmt.Sprintf("%s/%s", kind, name), "kind", kind, "name", name)
|
||||
util.EnsureNewline(b)
|
||||
header := fmt.Sprintf("---\n# Source: CUE apiObjects.%s.%s\n", kind, name)
|
||||
b = append(b, []byte(header+yamlString)...)
|
||||
util.EnsureNewline(b)
|
||||
}
|
||||
}
|
||||
r.finalOutput = string(b)
|
||||
}
|
||||
|
||||
// Save writes the content to the filesystem for git ops.
|
||||
func (r *Result) Save(ctx context.Context, path string, content string) error {
|
||||
log := logger.FromContext(ctx)
|
||||
@@ -215,6 +256,7 @@ func (b *Builder) Run(ctx context.Context) (results []*Result, err error) {
|
||||
if err := value.Decode(&result); err != nil {
|
||||
return nil, wrapper.Wrap(fmt.Errorf("could not decode: %w", err))
|
||||
}
|
||||
result.addOverlayObjects(log)
|
||||
case Helm:
|
||||
var helmChart HelmChart
|
||||
// First decode into the result. Helm will populate the api objects later.
|
||||
@@ -229,15 +271,7 @@ func (b *Builder) Run(ctx context.Context) (results []*Result, err error) {
|
||||
if err := runHelm(ctx, &helmChart, &result, holos.PathComponent(instance.Dir)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Append any cue api objects defined alongside the helm holos component.
|
||||
if helmChart.Content != "" && helmChart.ContentType == "application/yaml" {
|
||||
buf := []byte(result.Content)
|
||||
util.EnsureNewline(buf)
|
||||
buf = append(buf, []byte("---\n# Source: holos component overlay objects\n")...)
|
||||
buf = append(buf, []byte(helmChart.Content)...)
|
||||
log.DebugContext(ctx, "added additional api objects", "bytes", len(buf))
|
||||
result.Content = string(buf)
|
||||
}
|
||||
result.addOverlayObjects(log)
|
||||
|
||||
default:
|
||||
return nil, wrapper.Wrap(fmt.Errorf("build kind not implemented: %v", kind))
|
||||
@@ -300,6 +334,10 @@ func runCmd(ctx context.Context, name string, args ...string) (result runResult,
|
||||
// the rendered kubernetes api objects in the result.
|
||||
func runHelm(ctx context.Context, hc *HelmChart, r *Result, path holos.PathComponent) error {
|
||||
log := logger.FromContext(ctx).With("chart", hc.Chart.Name)
|
||||
if hc.Chart.Name == "" {
|
||||
log.WarnContext(ctx, "skipping helm: no chart name specified, use a different component type")
|
||||
return nil
|
||||
}
|
||||
|
||||
cachedChartPath := filepath.Join(string(path), ChartDir, hc.Chart.Name)
|
||||
if isNotExist(cachedChartPath) {
|
||||
@@ -342,7 +380,7 @@ func runHelm(ctx context.Context, hc *HelmChart, r *Result, path holos.PathCompo
|
||||
return wrapper.Wrap(fmt.Errorf("could not run helm template: %w", err))
|
||||
}
|
||||
|
||||
r.Content = helmOut.stdout.String()
|
||||
r.finalOutput = helmOut.stdout.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
47
|
||||
48
|
||||
|
||||
@@ -1 +1 @@
|
||||
2
|
||||
1
|
||||
|
||||
Reference in New Issue
Block a user