mirror of
https://github.com/holos-run/holos.git
synced 2026-03-19 16:54:58 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c76061b0d | ||
|
|
f60db8fa1f | ||
|
|
eefc092ea9 | ||
|
|
0860ac3409 | ||
|
|
6b156e9883 | ||
|
|
4de9f77fbf | ||
|
|
4c5429b64a | ||
|
|
ac5bff4b32 | ||
|
|
6090ab224e | ||
|
|
10e140258d | ||
|
|
40ac705f0d | ||
|
|
b4ad6425e5 | ||
|
|
3343d226e5 |
16
cmd/holos/testdata/issue15_cue_errors.txt
vendored
Normal file
16
cmd/holos/testdata/issue15_cue_errors.txt
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Want cue errors to show files and lines
|
||||
! exec holos build .
|
||||
stderr '^apiObjectMap.foo.bar: cannot convert non-concrete value string'
|
||||
stderr '/component.cue:7:20$'
|
||||
|
||||
-- cue.mod --
|
||||
package holos
|
||||
-- component.cue --
|
||||
package holos
|
||||
|
||||
apiVersion: "holos.run/v1alpha1"
|
||||
kind: "KubernetesObjects"
|
||||
cluster: string @tag(cluster, 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" }
|
||||
}
|
||||
}
|
||||
@@ -922,7 +922,7 @@ import (
|
||||
kubernetes?: {
|
||||
// Auth configures how secret-manager authenticates with a
|
||||
// Kubernetes instance.
|
||||
auth: struct.MaxFields(1) & {
|
||||
auth: {
|
||||
// has both clientCert and clientKey as secretKeySelector
|
||||
cert?: {
|
||||
// A reference to a specific 'key' within a Secret resource,
|
||||
|
||||
@@ -3,6 +3,7 @@ package holos
|
||||
// PlatformNamespace is a namespace to manage for Secret provisioning, SecretStore, etc...
|
||||
#PlatformNamespace: {
|
||||
name: string
|
||||
labels?: {[string]: string}
|
||||
}
|
||||
|
||||
// #PlatformNamespaces is a list of namespaces to manage across the platform.
|
||||
|
||||
@@ -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"
|
||||
@@ -15,6 +20,10 @@ objects: #CredsRefresherService.objects
|
||||
|
||||
#TargetNamespace: #CredsRefresher.namespace
|
||||
|
||||
#Kustomization: spec: {
|
||||
dependsOn: [{name: #InstancePrefix + "-namespaces"}]
|
||||
}
|
||||
|
||||
let NAME = #CredsRefresher.name
|
||||
let AUD = "//iam.googleapis.com/projects/\(#InputKeys.gcpProjectNumber)/locations/global/workloadIdentityPools/holos/providers/k8s-\(#InputKeys.cluster)"
|
||||
let MOUNT = "/var/run/service-account"
|
||||
@@ -0,0 +1,30 @@
|
||||
package holos
|
||||
|
||||
// Manages the External Secrets Operator from the official upstream Helm chart.
|
||||
|
||||
#TargetNamespace: "external-secrets"
|
||||
|
||||
#InputKeys: component: "eso"
|
||||
|
||||
#InputKeys: {
|
||||
project: "secrets"
|
||||
service: "eso"
|
||||
}
|
||||
|
||||
#Kustomization: spec: {
|
||||
dependsOn: [{name: #InstancePrefix + "-namespaces"}]
|
||||
targetNamespace: #TargetNamespace
|
||||
}
|
||||
|
||||
#HelmChart & {
|
||||
values: installCrds: true
|
||||
namespace: #TargetNamespace
|
||||
chart: {
|
||||
name: "external-secrets"
|
||||
version: "0.9.12"
|
||||
repository: {
|
||||
name: "external-secrets"
|
||||
url: "https://charts.external-secrets.io"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package holos
|
||||
|
||||
import "list"
|
||||
|
||||
#TargetNamespace: "default"
|
||||
|
||||
#InputKeys: {
|
||||
@@ -14,18 +12,18 @@ import "list"
|
||||
_ns: #PlatformNamespace
|
||||
|
||||
objects: [
|
||||
#Namespace & {
|
||||
metadata: name: _ns.name
|
||||
},
|
||||
#Namespace & {metadata: _ns},
|
||||
]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package holos
|
||||
|
||||
// Validate ESO by syncing a secret with a SecretStore.
|
||||
|
||||
#TargetNamespace: "holos-system"
|
||||
|
||||
#InputKeys: {
|
||||
project: "secrets"
|
||||
component: "validate"
|
||||
}
|
||||
|
||||
#Kustomization: spec: dependsOn: [{name: #InstancePrefix + "-eso"}]
|
||||
|
||||
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
SecretStore: default: #SecretStore
|
||||
|
||||
ExternalSecret: validate: #ExternalSecret & {
|
||||
_name: "validate"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package holos
|
||||
|
||||
// Manage Ceph CSI to provide PersistentVolumeClaims to a cluster.
|
||||
|
||||
#TargetNamespace: "ceph-system"
|
||||
|
||||
#SecretName: "\(#ClusterName)-ceph-csi-rbd"
|
||||
|
||||
#InputKeys: {
|
||||
project: "metal"
|
||||
service: "ceph"
|
||||
component: "ceph"
|
||||
}
|
||||
|
||||
#Kustomization: spec: {
|
||||
dependsOn: [{name: "prod-secrets-namespaces"}]
|
||||
targetNamespace: #TargetNamespace
|
||||
}
|
||||
|
||||
#HelmChart & {
|
||||
namespace: #TargetNamespace
|
||||
chart: {
|
||||
name: "ceph-csi-rbd"
|
||||
version: "3.10.2"
|
||||
repository: {
|
||||
name: "ceph-csi"
|
||||
url: "https://ceph.github.io/csi-charts"
|
||||
}
|
||||
}
|
||||
|
||||
apiObjects: {
|
||||
SecretStore: default: #SecretStore
|
||||
ExternalSecret: "\(#SecretName)": #ExternalSecret & {
|
||||
_name: #SecretName
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package holos
|
||||
|
||||
#Input: {
|
||||
config: {
|
||||
// (required) String representing a Ceph cluster to provision storage from.
|
||||
// Should be unique across all Ceph clusters in use for provisioning,
|
||||
// cannot be greater than 36 bytes in length, and should remain immutable for
|
||||
// the lifetime of the StorageClass in use.
|
||||
clusterID: string
|
||||
// (required) []String list of ceph monitor "address:port" values.
|
||||
monitors: [...string]
|
||||
}
|
||||
}
|
||||
|
||||
// Imported from https://github.com/holos-run/holos-infra/blob/0ae58858f5583d25fa7543e47b5f5e9f0b2f3c83/components/core/metal/ceph-csi-rbd/values.holos.yaml
|
||||
|
||||
#ChartValues: {
|
||||
// Necessary for Talos see https://github.com/siderolabs/talos/discussions/8163
|
||||
selinuxMount: false
|
||||
|
||||
csiConfig: [#Input.config]
|
||||
|
||||
storageClass: {
|
||||
annotations: "storageclass.kubernetes.io/is-default-class": "true"
|
||||
|
||||
// Specifies whether the storageclass should be created
|
||||
create: true
|
||||
name: "ceph-ssd"
|
||||
|
||||
// (optional) Prefix to use for naming RBD images.
|
||||
// If omitted, defaults to "csi-vol-".
|
||||
// NOTE: Set this to a cluster specific value, e.g. vol-k1-
|
||||
volumeNamePrefix: "vol-\(#ClusterName)-"
|
||||
|
||||
// (required) String representing a Ceph cluster to provision storage from.
|
||||
// Should be unique across all Ceph clusters in use for provisioning,
|
||||
// cannot be greater than 36 bytes in length, and should remain immutable for
|
||||
// the lifetime of the StorageClass in use.
|
||||
clusterID: #Input.config.clusterID
|
||||
|
||||
// (optional) If you want to use erasure coded pool with RBD, you need to
|
||||
// create two pools. one erasure coded and one replicated.
|
||||
// You need to specify the replicated pool here in the `pool` parameter, it is
|
||||
// used for the metadata of the images.
|
||||
// The erasure coded pool must be set as the `dataPool` parameter below.
|
||||
// dataPool: <ec-data-pool>
|
||||
dataPool: ""
|
||||
|
||||
// (required) Ceph pool into which the RBD image shall be created
|
||||
// eg: pool: replicapool
|
||||
pool: "k8s-dev"
|
||||
|
||||
// (optional) RBD image features, CSI creates image with image-format 2 CSI
|
||||
// RBD currently supports `layering`, `journaling`, `exclusive-lock`,
|
||||
// `object-map`, `fast-diff`, `deep-flatten` features.
|
||||
// Refer https://docs.ceph.com/en/latest/rbd/rbd-config-ref/#image-features
|
||||
// for image feature dependencies.
|
||||
// imageFeatures: layering,journaling,exclusive-lock,object-map,fast-diff
|
||||
imageFeatures: "layering"
|
||||
|
||||
// (optional) Specifies whether to try other mounters in case if the current
|
||||
// mounter fails to mount the rbd image for any reason. True means fallback
|
||||
// to next mounter, default is set to false.
|
||||
// Note: tryOtherMounters is currently useful to fallback from krbd to rbd-nbd
|
||||
// in case if any of the specified imageFeatures is not supported by krbd
|
||||
// driver on node scheduled for application pod launch, but in the future this
|
||||
// should work with any mounter type.
|
||||
// tryOtherMounters: false
|
||||
// (optional) uncomment the following to use rbd-nbd as mounter
|
||||
// on supported nodes
|
||||
// mounter: rbd-nbd
|
||||
mounter: ""
|
||||
|
||||
// (optional) ceph client log location, eg: rbd-nbd
|
||||
// By default host-path /var/log/ceph of node is bind-mounted into
|
||||
// csi-rbdplugin pod at /var/log/ceph mount path. This is to configure
|
||||
// target bindmount path used inside container for ceph clients logging.
|
||||
// See docs/rbd-nbd.md for available configuration options.
|
||||
// cephLogDir: /var/log/ceph
|
||||
cephLogDir: ""
|
||||
|
||||
// (optional) ceph client log strategy
|
||||
// By default, log file belonging to a particular volume will be deleted
|
||||
// on unmap, but you can choose to just compress instead of deleting it
|
||||
// or even preserve the log file in text format as it is.
|
||||
// Available options `remove` or `compress` or `preserve`
|
||||
// cephLogStrategy: remove
|
||||
cephLogStrategy: ""
|
||||
|
||||
// (optional) Instruct the plugin it has to encrypt the volume
|
||||
// By default it is disabled. Valid values are "true" or "false".
|
||||
// A string is expected here, i.e. "true", not true.
|
||||
// encrypted: "true"
|
||||
encrypted: ""
|
||||
|
||||
// (optional) Use external key management system for encryption passphrases by
|
||||
// specifying a unique ID matching KMS ConfigMap. The ID is only used for
|
||||
// correlation to configmap entry.
|
||||
encryptionKMSID: ""
|
||||
|
||||
// Add topology constrained pools configuration, if topology based pools
|
||||
// are setup, and topology constrained provisioning is required.
|
||||
// For further information read TODO<doc>
|
||||
// topologyConstrainedPools: |
|
||||
// [{"poolName":"pool0",
|
||||
// "dataPool":"ec-pool0" # optional, erasure-coded pool for data
|
||||
// "domainSegments":[
|
||||
// {"domainLabel":"region","value":"east"},
|
||||
// {"domainLabel":"zone","value":"zone1"}]},
|
||||
// {"poolName":"pool1",
|
||||
// "dataPool":"ec-pool1" # optional, erasure-coded pool for data
|
||||
// "domainSegments":[
|
||||
// {"domainLabel":"region","value":"east"},
|
||||
// {"domainLabel":"zone","value":"zone2"}]},
|
||||
// {"poolName":"pool2",
|
||||
// "dataPool":"ec-pool2" # optional, erasure-coded pool for data
|
||||
// "domainSegments":[
|
||||
// {"domainLabel":"region","value":"west"},
|
||||
// {"domainLabel":"zone","value":"zone1"}]}
|
||||
// ]
|
||||
topologyConstrainedPools: []
|
||||
|
||||
// (optional) mapOptions is a comma-separated list of map options.
|
||||
// For krbd options refer
|
||||
// https://docs.ceph.com/docs/master/man/8/rbd/#kernel-rbd-krbd-options
|
||||
// For nbd options refer
|
||||
// https://docs.ceph.com/docs/master/man/8/rbd-nbd/#options
|
||||
// Format:
|
||||
// mapOptions: "<mounter>:op1,op2;<mounter>:op1,op2"
|
||||
// An empty mounter field is treated as krbd type for compatibility.
|
||||
// eg:
|
||||
// mapOptions: "krbd:lock_on_read,queue_depth=1024;nbd:try-netlink"
|
||||
mapOptions: ""
|
||||
|
||||
// (optional) unmapOptions is a comma-separated list of unmap options.
|
||||
// For krbd options refer
|
||||
// https://docs.ceph.com/docs/master/man/8/rbd/#kernel-rbd-krbd-options
|
||||
// For nbd options refer
|
||||
// https://docs.ceph.com/docs/master/man/8/rbd-nbd/#options
|
||||
// Format:
|
||||
// unmapOptions: "<mounter>:op1,op2;<mounter>:op1,op2"
|
||||
// An empty mounter field is treated as krbd type for compatibility.
|
||||
// eg:
|
||||
// unmapOptions: "krbd:force;nbd:force"
|
||||
unmapOptions: ""
|
||||
|
||||
// The secrets have to contain Ceph credentials with required access
|
||||
// to the 'pool'.
|
||||
provisionerSecret: #SecretName
|
||||
// If Namespaces are left empty, the secrets are assumed to be in the
|
||||
// Release namespace.
|
||||
provisionerSecretNamespace: ""
|
||||
controllerExpandSecret: #SecretName
|
||||
controllerExpandSecretNamespace: ""
|
||||
nodeStageSecret: #SecretName
|
||||
nodeStageSecretNamespace: ""
|
||||
// Specify the filesystem type of the volume. If not specified,
|
||||
// csi-provisioner will set default as `ext4`.
|
||||
fstype: "ext4"
|
||||
reclaimPolicy: "Delete"
|
||||
allowVolumeExpansion: true
|
||||
mountOptions: []
|
||||
}
|
||||
|
||||
secret: {
|
||||
// Specifies whether the secret should be created
|
||||
create: false
|
||||
name: #SecretName
|
||||
// Key values correspond to a user name and its key, as defined in the
|
||||
// ceph cluster. User ID should have required access to the 'pool'
|
||||
// specified in the storage class
|
||||
userID: "admin"
|
||||
userKey: "$(ceph auth get-key client.admin)"
|
||||
// Encryption passphrase
|
||||
encryptionPassphrase: "$(python -c 'import secrets; print(secrets.token_hex(32));')"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package holos
|
||||
|
||||
#Input: {
|
||||
config: {
|
||||
clusterID: "a6de32ab-c84f-49a6-b97e-e31dc2a70931"
|
||||
monitors: ["10.64.1.21:6789", "10.64.1.31:6789", "10.64.1.41:6789"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Metal Clusters
|
||||
|
||||
This cluster type is overlaid onto other cluster types to add services necessary outside of a cloud like GKE or EKS. Ceph for PersistenVolumeClaim support on a Talos Proxmox cluster is the primary use case.
|
||||
|
||||
## Test Script
|
||||
|
||||
Test ceph is working with:
|
||||
|
||||
```bash
|
||||
apply -n default -f-<<EOF
|
||||
heredoc> apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
volumeMode: Filesystem
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
EOF
|
||||
```
|
||||
@@ -1,3 +0,0 @@
|
||||
package holos
|
||||
|
||||
#InputKeys: component: "eso"
|
||||
@@ -1,8 +0,0 @@
|
||||
package holos
|
||||
|
||||
#TargetNamespace: "external-secrets"
|
||||
|
||||
#InputKeys: {
|
||||
project: "secrets"
|
||||
service: "eso"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package holos
|
||||
|
||||
#Kustomization: spec: dependsOn: [{name: #InstancePrefix + "-namespaces"}]
|
||||
|
||||
#HelmChart & {
|
||||
values: installCrds: true
|
||||
namespace: #TargetNamespace
|
||||
chart: {
|
||||
name: "external-secrets"
|
||||
version: "0.9.12"
|
||||
repository: {
|
||||
name: "external-secrets"
|
||||
url: "https://charts.external-secrets.io"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
package holos
|
||||
|
||||
// #PlatformNamespaces is the union of all namespaces across all cluster types. Namespaces are created in all clusters regardless of if they're
|
||||
// used within the cluster or not. The is important for security and consistency with IAM, RBAC, and Secrets sync between clusters.
|
||||
#PlatformNamespaces: [
|
||||
{name: "external-secrets"},
|
||||
{name: "holos-system"},
|
||||
{name: "flux-system"},
|
||||
{name: "ceph-system"},
|
||||
{
|
||||
name: "ceph-system"
|
||||
labels: "pod-security.kubernetes.io/enforce": "privileged"
|
||||
},
|
||||
{name: "istio-system"},
|
||||
{name: "istio-ingress"},
|
||||
{name: "cert-manager"},
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package holos
|
||||
|
||||
#Kustomization: spec: dependsOn: [{name: #InstancePrefix + "-eso"}]
|
||||
|
||||
objects: [
|
||||
#SecretStore,
|
||||
#ExternalSecret & {
|
||||
_name: "validate"
|
||||
spec: dataFrom: [{extract: key: "ns/" + #TargetNamespace + "/test"}]
|
||||
},
|
||||
]
|
||||
|
||||
{} & #KubernetesObjects
|
||||
@@ -1,8 +0,0 @@
|
||||
package holos
|
||||
|
||||
#TargetNamespace: "default"
|
||||
|
||||
#InputKeys: {
|
||||
project: "secrets"
|
||||
component: "validate"
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import (
|
||||
"encoding/yaml"
|
||||
)
|
||||
|
||||
// #ClusterName is the cluster name for cluster scoped resources.
|
||||
#ClusterName: #InputKeys.cluster
|
||||
|
||||
_apiVersion: "holos.run/v1alpha1"
|
||||
|
||||
// #Name defines the name: string key value pair used all over the place.
|
||||
@@ -30,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
|
||||
...
|
||||
}
|
||||
|
||||
@@ -79,8 +86,10 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
kind: string | *"GitRepository"
|
||||
name: string | *"flux-system"
|
||||
}
|
||||
timeout: string | *"3m0s"
|
||||
wait: bool | *true
|
||||
suspend?: bool
|
||||
targetNamespace?: string
|
||||
timeout: string | *"3m0s"
|
||||
wait: bool | *true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,8 +97,8 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
#ExternalSecret: #NamespaceObject & es.#ExternalSecret & {
|
||||
_name: string
|
||||
metadata: {
|
||||
namespace: string | *"default"
|
||||
name: _name
|
||||
namespace: #TargetNamespace
|
||||
}
|
||||
spec: {
|
||||
refreshInterval: string | *"1h"
|
||||
@@ -98,26 +107,31 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
name: string | *"default"
|
||||
}
|
||||
target: {
|
||||
name: _name
|
||||
creationPolicy: string | *"Owner"
|
||||
deletionPolicy: string | *"Retain"
|
||||
}
|
||||
// Copy fields 1:1 from external Secret to target Secret.
|
||||
dataFrom: [{extract: key: _name}]
|
||||
}
|
||||
}
|
||||
|
||||
#SecretStore: #NamespaceObject & ss.#SecretStore & {
|
||||
metadata: {
|
||||
name: string | *"default"
|
||||
namespace: string | *#TargetNamespace
|
||||
namespace: #TargetNamespace
|
||||
}
|
||||
spec: provider: {
|
||||
vault: {
|
||||
auth: kubernetes: {
|
||||
mountPath: #InputKeys.cluster
|
||||
role: string | *"default"
|
||||
serviceAccountRef: name: string | *"default"
|
||||
kubernetes: {
|
||||
remoteNamespace: #TargetNamespace
|
||||
auth: token: bearerToken: {
|
||||
name: string | *"eso-reader"
|
||||
key: string | *"token"
|
||||
}
|
||||
server: {
|
||||
caBundle: #InputKeys.provisionerCABundle
|
||||
url: #InputKeys.provisionerURL
|
||||
}
|
||||
path: string | *"kv/k8s"
|
||||
server: "https://vault.core." + #Platform.org.domain
|
||||
version: string | *"v2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,6 +152,11 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
// GCP Project Info used for the Provisioner Cluster
|
||||
gcpProjectID: string @tag(gcpProjectID, type=string)
|
||||
gcpProjectNumber: int @tag(gcpProjectNumber, type=int)
|
||||
|
||||
// Same as cluster certificate-authority-data field in ~/.holos/kubeconfig.provisioner
|
||||
provisionerCABundle: string @tag(provisionerCABundle, type=string)
|
||||
// Same as the cluster server field in ~/.holos/kubeconfig.provisioner
|
||||
provisionerURL: string @tag(provisionerURL, type=string)
|
||||
}
|
||||
|
||||
// #Platform defines the primary lookup table for the platform. Lookup keys should be limited to those defined in #KeyTags.
|
||||
@@ -163,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
|
||||
@@ -182,13 +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] | *[]
|
||||
// out holds the rendered yaml text stream of kubernetes api objects.
|
||||
content: yaml.MarshalStream(objects)
|
||||
// ksObjects holds the flux Kustomization objects for gitops
|
||||
ksObjects: [...#Kustomization] | *[#Kustomization]
|
||||
// ksContent is the yaml representation of kustomization
|
||||
@@ -197,6 +236,8 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
platform: #Platform
|
||||
}
|
||||
|
||||
objects: "not allowed"
|
||||
|
||||
// #Chart defines an upstream helm chart
|
||||
#Chart: {
|
||||
name: string
|
||||
@@ -207,9 +248,14 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
}
|
||||
}
|
||||
|
||||
// #ChartValues represent the values provided to a helm chart. Existing values may be imorted using cue import values.yaml -p holos then wrapping the values.cue content in #Values: {}
|
||||
#ChartValues: {...}
|
||||
|
||||
// #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]
|
||||
@@ -220,7 +266,7 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
// chart defines the upstream helm chart to process.
|
||||
chart: #Chart
|
||||
// values represents the helm values to provide to the chart.
|
||||
values: {...}
|
||||
values: #ChartValues
|
||||
// valuesContent holds the values yaml
|
||||
valuesContent: yaml.Marshal(values)
|
||||
// platform returns the platform data structure for visibility / troubleshooting.
|
||||
@@ -239,3 +285,6 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
|
||||
// Holos component name
|
||||
metadata: name: #InstanceName
|
||||
|
||||
// #SecretName is the name of a Secret, ususally coupling a Deployment to an ExternalSecret
|
||||
#SecretName: string
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -27,11 +27,3 @@ func New(name string) *cobra.Command {
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// EnsureNewline adds a trailing newline if not already there.
|
||||
func EnsureNewline(b []byte) []byte {
|
||||
if len(b) > 0 && b[len(b)-1] != '\n' {
|
||||
b = append(b, '\n')
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/holos-run/holos/pkg/cli/secret"
|
||||
"github.com/holos-run/holos/pkg/holos"
|
||||
"github.com/holos-run/holos/pkg/logger"
|
||||
"github.com/holos-run/holos/pkg/util"
|
||||
"github.com/holos-run/holos/pkg/wrapper"
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -77,7 +78,7 @@ func makeGetRunFunc(cfg *holos.Config, cf getConfig) command.RunFunc {
|
||||
// Print one file to stdout
|
||||
if key := *cf.file; key != "" {
|
||||
if data, found := secret.Data[key]; found {
|
||||
cfg.Write(command.EnsureNewline(data))
|
||||
cfg.Write(util.EnsureNewline(data))
|
||||
return nil
|
||||
}
|
||||
return wrapper.Wrap(fmt.Errorf("not found: %s have %#v", key, keys))
|
||||
@@ -89,7 +90,7 @@ func makeGetRunFunc(cfg *holos.Config, cf getConfig) command.RunFunc {
|
||||
|
||||
for k, v := range secret.Data {
|
||||
cfg.Printf("-- %s --\n", k)
|
||||
cfg.Write(command.EnsureNewline(v))
|
||||
cfg.Write(util.EnsureNewline(v))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -2,7 +2,8 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"cuelang.org/go/cue/errors"
|
||||
"fmt"
|
||||
"github.com/holos-run/holos/pkg/holos"
|
||||
"github.com/holos-run/holos/pkg/wrapper"
|
||||
"log/slog"
|
||||
@@ -15,21 +16,27 @@ func MakeMain(options ...holos.Option) func() int {
|
||||
slog.SetDefault(cfg.Logger())
|
||||
ctx := context.Background()
|
||||
if err := New(cfg).ExecuteContext(ctx); err != nil {
|
||||
return handleError(ctx, err, cfg)
|
||||
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) {
|
||||
// HandleError is the top level error handler that unwraps and logs errors.
|
||||
func HandleError(ctx context.Context, err error, hc *holos.Config) (exitCode int) {
|
||||
log := hc.NewTopLevelLogger()
|
||||
var cueErr errors.Error
|
||||
var errAt *wrapper.ErrorAt
|
||||
const msg = "could not execute"
|
||||
if ok := errors.As(err, &errAt); ok {
|
||||
if errors.As(err, &errAt) {
|
||||
log.ErrorContext(ctx, msg, "err", errAt.Unwrap(), "loc", errAt.Source.Loc())
|
||||
} else {
|
||||
log.ErrorContext(ctx, msg, "err", err)
|
||||
}
|
||||
// cue errors are bundled up as a list and refer to multiple files / lines.
|
||||
if errors.As(err, &cueErr) {
|
||||
msg := errors.Details(cueErr, nil)
|
||||
_, _ = fmt.Fprint(hc.Stderr(), msg)
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/holos-run/holos/pkg/logger"
|
||||
"github.com/holos-run/holos/pkg/wrapper"
|
||||
"github.com/spf13/cobra"
|
||||
"io"
|
||||
"io/fs"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -26,6 +27,8 @@ func NewCreateCmd(hc *holos.Config) *cobra.Command {
|
||||
cfg, flagSet := newConfig()
|
||||
flagSet.Var(&cfg.files, "from-file", "store files as keys in the secret")
|
||||
cfg.dryRun = flagSet.Bool("dry-run", false, "dry run")
|
||||
cfg.appendHash = flagSet.Bool("append-hash", true, "append hash to kubernetes secret name")
|
||||
cfg.dataStdin = flagSet.Bool("data-stdin", false, "read data field as json from stdin if")
|
||||
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.Flags().AddGoFlagSet(flagSet)
|
||||
@@ -45,8 +48,9 @@ func makeCreateRunFunc(hc *holos.Config, cfg *config) command.RunFunc {
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Labels: map[string]string{NameLabel: secretName},
|
||||
Name: secretName,
|
||||
Namespace: *cfg.namespace,
|
||||
Labels: map[string]string{NameLabel: secretName},
|
||||
},
|
||||
Data: make(secretData),
|
||||
}
|
||||
@@ -59,6 +63,22 @@ func makeCreateRunFunc(hc *holos.Config, cfg *config) command.RunFunc {
|
||||
}
|
||||
}
|
||||
|
||||
if *cfg.dataStdin {
|
||||
log.InfoContext(ctx, "reading data keys from stdin...")
|
||||
var obj map[string]string
|
||||
data, err := io.ReadAll(hc.Stdin())
|
||||
if err != nil {
|
||||
return wrapper.Wrap(err)
|
||||
}
|
||||
err = yaml.Unmarshal(data, &obj)
|
||||
if err != nil {
|
||||
return wrapper.Wrap(err)
|
||||
}
|
||||
for k, v := range obj {
|
||||
secret.Data[k] = []byte(v)
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range cfg.files {
|
||||
if err := filepath.WalkDir(file, makeWalkFunc(secret.Data, file)); err != nil {
|
||||
return wrapper.Wrap(err)
|
||||
@@ -72,10 +92,12 @@ func makeCreateRunFunc(hc *holos.Config, cfg *config) command.RunFunc {
|
||||
secret.Labels[ClusterLabel] = *cfg.cluster
|
||||
}
|
||||
|
||||
if secretHash, err := hash.SecretHash(secret); err != nil {
|
||||
return wrapper.Wrap(err)
|
||||
} else {
|
||||
secret.Name = fmt.Sprintf("%s-%s", secret.Name, secretHash)
|
||||
if *cfg.appendHash {
|
||||
if secretHash, err := hash.SecretHash(secret); err != nil {
|
||||
return wrapper.Wrap(err)
|
||||
} else {
|
||||
secret.Name = fmt.Sprintf("%s-%s", secret.Name, secretHash)
|
||||
}
|
||||
}
|
||||
|
||||
if *cfg.dryRun {
|
||||
|
||||
@@ -63,7 +63,7 @@ func makeGetRunFunc(hc *holos.Config, cfg *config) command.RunFunc {
|
||||
|
||||
log.DebugContext(ctx, "results", "len", len(list.Items))
|
||||
if len(list.Items) < 1 {
|
||||
continue
|
||||
return wrapper.Wrap(fmt.Errorf("not found: %v", secretName))
|
||||
}
|
||||
|
||||
// Sort oldest first.
|
||||
|
||||
@@ -12,13 +12,15 @@ const ClusterLabel = "holos.run/cluster.name"
|
||||
type secretData map[string][]byte
|
||||
|
||||
type config struct {
|
||||
files holos.StringSlice
|
||||
printFile *string
|
||||
extract *bool
|
||||
dryRun *bool
|
||||
cluster *string
|
||||
namespace *string
|
||||
extractTo *string
|
||||
files holos.StringSlice
|
||||
printFile *string
|
||||
extract *bool
|
||||
dryRun *bool
|
||||
appendHash *bool
|
||||
dataStdin *bool
|
||||
cluster *string
|
||||
namespace *string
|
||||
extractTo *string
|
||||
}
|
||||
|
||||
func newConfig() (*config, *flag.FlagSet) {
|
||||
|
||||
@@ -55,11 +55,12 @@ func cmdHolos(ts *testscript.TestScript, neg bool, args []string) {
|
||||
cmd := cli.New(cfg)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
|
||||
if neg {
|
||||
if err == nil {
|
||||
ts.Fatalf("want: error\nhave: %v", err)
|
||||
ts.Fatalf("\nwant: error\nhave: %v", err)
|
||||
} else {
|
||||
ts.Logf("want: error\nhave: %v", err)
|
||||
cli.HandleError(cmd.Context(), err, cfg)
|
||||
}
|
||||
} else {
|
||||
ts.Check(err)
|
||||
|
||||
7
pkg/cli/secret/testdata/create_secret_no_hash.txt
vendored
Normal file
7
pkg/cli/secret/testdata/create_secret_no_hash.txt
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Want no hash appended
|
||||
holos create secret test --namespace holos-system --from-file $WORK/test --append-hash=false
|
||||
stderr ' created: test '
|
||||
stderr ' secret=test '
|
||||
|
||||
-- test --
|
||||
sekret
|
||||
6
pkg/cli/secret/testdata/create_secret_no_hash_dry_run.txt
vendored
Normal file
6
pkg/cli/secret/testdata/create_secret_no_hash_dry_run.txt
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Want no hash appended
|
||||
holos create secret test --namespace holos-system --from-file $WORK/test --append-hash=false --dry-run
|
||||
stdout 'name: test$'
|
||||
|
||||
-- test --
|
||||
sekret
|
||||
3
pkg/cli/secret/testdata/issue20_secret_not_found.txt
vendored
Normal file
3
pkg/cli/secret/testdata/issue20_secret_not_found.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Want missing secrets to exit non-zero https://github.com/holos-run/holos/issues/20
|
||||
! holos get secret does-not-exist
|
||||
stderr 'not found: does-not-exist'
|
||||
@@ -67,9 +67,9 @@ func printFile(w io.Writer, idx int, a *txtar.Archive) (err error) {
|
||||
return wrapper.Wrap(fmt.Errorf("idx cannot be 0"))
|
||||
}
|
||||
if idx > 0 {
|
||||
_, err = w.Write(command.EnsureNewline(a.Files[idx-1].Data))
|
||||
_, err = w.Write(util.EnsureNewline(a.Files[idx-1].Data))
|
||||
} else {
|
||||
_, err = w.Write(command.EnsureNewline(a.Files[len(a.Files)+idx].Data))
|
||||
_, err = w.Write(util.EnsureNewline(a.Files[len(a.Files)+idx].Data))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,10 +10,13 @@ import (
|
||||
"fmt"
|
||||
"github.com/holos-run/holos"
|
||||
"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"
|
||||
@@ -72,11 +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"`
|
||||
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 {
|
||||
@@ -92,13 +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"`
|
||||
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
|
||||
@@ -115,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)
|
||||
@@ -211,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.
|
||||
@@ -225,6 +271,8 @@ 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
|
||||
}
|
||||
result.addOverlayObjects(log)
|
||||
|
||||
default:
|
||||
return nil, wrapper.Wrap(fmt.Errorf("build kind not implemented: %v", kind))
|
||||
}
|
||||
@@ -286,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) {
|
||||
@@ -328,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
|
||||
}
|
||||
|
||||
9
pkg/util/util.go
Normal file
9
pkg/util/util.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package util
|
||||
|
||||
// EnsureNewline adds a trailing newline if not already there.
|
||||
func EnsureNewline(b []byte) []byte {
|
||||
if len(b) > 0 && b[len(b)-1] != '\n' {
|
||||
b = append(b, '\n')
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
45
|
||||
48
|
||||
|
||||
@@ -1 +1 @@
|
||||
2
|
||||
1
|
||||
|
||||
Reference in New Issue
Block a user