Compare commits

..

2 Commits

Author SHA1 Message Date
Jeff McCune
8c76061b0d (#27) Add recommended labels and sort output
Add the recommended labels mapping to holos stage, project, and
component names.  Project will eventually be renamed to "collection" or
something.

Example:

    app.kubernetes.io/part-of: prod
    app.kubernetes.io/name: secrets
    app.kubernetes.io/component: validate
    app.kubernetes.io/instance: prod-secrets-validate

Also sort the api objects produced from cue so the output of the `holos
render` command is stable for git commits.
2024-02-29 15:12:19 -08:00
Jeff McCune
f60db8fa1f (#25) Show name of api object in errors
This patch changes the interface between CUE and Holos to remove the
content field and replace it with an api object map.  The map is a
`map[string]map[string]string` with the rendered yaml as the value of a
kind/name nesting.

This structure enables better error messages, cue disjunction errors
indicate the type and the name of the resource instead of just the list
index number.
2024-02-29 11:23:49 -08:00
16 changed files with 304 additions and 91 deletions

View File

@@ -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

View 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)
}
}
}
}
}

View 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)
}
}
}
}
}

View 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" }
}
}

View File

@@ -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"

View File

@@ -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
}
}
}
}

View File

@@ -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"

View File

@@ -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
}
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
}

View File

@@ -1 +1 @@
47
48

View File

@@ -1 +1 @@
2
1