mirror of
https://github.com/holos-run/holos.git
synced 2026-03-19 16:54:58 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c5429b64a | ||
|
|
ac5bff4b32 | ||
|
|
6090ab224e | ||
|
|
10e140258d | ||
|
|
40ac705f0d | ||
|
|
b4ad6425e5 | ||
|
|
3343d226e5 | ||
|
|
f3a9b7cfbc | ||
|
|
53b7246d5e |
@@ -1,28 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/holos-run/holos/pkg/cli"
|
||||
"github.com/holos-run/holos/pkg/holos"
|
||||
"github.com/holos-run/holos/pkg/wrapper"
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := holos.New()
|
||||
slog.SetDefault(cfg.Logger())
|
||||
ctx := context.Background()
|
||||
if err := cli.New(cfg).ExecuteContext(ctx); err != nil {
|
||||
log := cfg.NewTopLevelLogger()
|
||||
var errAt *wrapper.ErrorAt
|
||||
const msg = "could not execute"
|
||||
if ok := errors.As(err, &errAt); ok {
|
||||
log.ErrorContext(ctx, msg, "err", errAt.Unwrap(), "loc", errAt.Source.Loc())
|
||||
} else {
|
||||
log.ErrorContext(ctx, msg, "err", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(cli.MakeMain()())
|
||||
}
|
||||
|
||||
20
cmd/holos/main_test.go
Normal file
20
cmd/holos/main_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/holos-run/holos/pkg/cli"
|
||||
"github.com/rogpeppe/go-internal/testscript"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(testscript.RunMain(m, map[string]func() int{
|
||||
"holos": cli.MakeMain(),
|
||||
}))
|
||||
}
|
||||
|
||||
func TestGetSecrets(t *testing.T) {
|
||||
testscript.Run(t, testscript.Params{
|
||||
Dir: "testdata",
|
||||
})
|
||||
}
|
||||
15
cmd/holos/testdata/issue15_cue_errors.txt
vendored
Normal file
15
cmd/holos/testdata/issue15_cue_errors.txt
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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$'
|
||||
|
||||
-- cue.mod --
|
||||
package holos
|
||||
-- component.cue --
|
||||
package holos
|
||||
|
||||
apiVersion: "holos.run/v1alpha1"
|
||||
kind: "KubernetesObjects"
|
||||
cluster: string @tag(cluster, string)
|
||||
content: foo
|
||||
foo: string
|
||||
5
cmd/holos/testdata/version.txt
vendored
Normal file
5
cmd/holos/testdata/version.txt
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
exec holos --version
|
||||
# want version with no v on stdout
|
||||
stdout -count=1 '^\d+\.\d+\.\d+$'
|
||||
# want nothing on stderr
|
||||
! stderr .
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package holos
|
||||
|
||||
// Manage Ceph CSI to provide PersistentVolumeClaims to a cluster.
|
||||
|
||||
#TargetNamespace: "ceph-system"
|
||||
|
||||
#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"
|
||||
}
|
||||
}
|
||||
objects: [#ExternalSecret & { _name: "ceph-csi-rbd" }]
|
||||
}
|
||||
@@ -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-\(#InputKeys.cluster)-"
|
||||
|
||||
// (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: "csi-rbd-secret"
|
||||
// If Namespaces are left empty, the secrets are assumed to be in the
|
||||
// Release namespace.
|
||||
provisionerSecretNamespace: ""
|
||||
controllerExpandSecret: "csi-rbd-secret"
|
||||
controllerExpandSecretNamespace: ""
|
||||
nodeStageSecret: "csi-rbd-secret"
|
||||
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: "csi-rbd-secret"
|
||||
// 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,3 @@
|
||||
# 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.
|
||||
@@ -15,6 +15,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"
|
||||
|
||||
@@ -1,3 +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,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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"}]
|
||||
|
||||
objects: [
|
||||
#SecretStore,
|
||||
#ExternalSecret & { _name: "validate" },
|
||||
]
|
||||
|
||||
{} & #KubernetesObjects
|
||||
@@ -1,6 +1,9 @@
|
||||
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"},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -79,8 +79,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,7 +90,7 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
#ExternalSecret: #NamespaceObject & es.#ExternalSecret & {
|
||||
_name: string
|
||||
metadata: {
|
||||
namespace: string | *"default"
|
||||
namespace: #TargetNamespace
|
||||
name: _name
|
||||
}
|
||||
spec: {
|
||||
@@ -100,24 +102,29 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
target: {
|
||||
creationPolicy: string | *"Owner"
|
||||
}
|
||||
data: [{
|
||||
remoteRef: key: _name
|
||||
secretKey: _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 +145,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.
|
||||
@@ -187,8 +199,9 @@ _apiVersion: "holos.run/v1alpha1"
|
||||
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 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
|
||||
@@ -207,6 +220,9 @@ _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
|
||||
@@ -220,13 +236,18 @@ _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.
|
||||
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.
|
||||
|
||||
3
go.mod
3
go.mod
@@ -17,6 +17,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/emicklei/proto v1.10.0 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/go-logr/logr v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
@@ -38,7 +39,9 @@ require (
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -13,6 +13,8 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw=
|
||||
github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
@@ -88,12 +90,16 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4=
|
||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c=
|
||||
github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c h1:fPpdjePK1atuOg28PXfNSqgwf9I/qD1Hlo39JFwKBXk=
|
||||
github.com/rogpeppe/go-internal v1.11.1-0.20231026093722-fa6a31e0812c/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
|
||||
@@ -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
|
||||
|
||||
42
pkg/cli/main.go
Normal file
42
pkg/cli/main.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cuelang.org/go/cue/errors"
|
||||
"fmt"
|
||||
"github.com/holos-run/holos/pkg/holos"
|
||||
"github.com/holos-run/holos/pkg/wrapper"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// 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 err := New(cfg).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) {
|
||||
log := hc.NewTopLevelLogger()
|
||||
var cueErr errors.Error
|
||||
var errAt *wrapper.ErrorAt
|
||||
const msg = "could not execute"
|
||||
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
|
||||
}
|
||||
@@ -26,6 +26,7 @@ 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")
|
||||
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.Flags().AddGoFlagSet(flagSet)
|
||||
@@ -40,6 +41,10 @@ func makeCreateRunFunc(hc *holos.Config, cfg *config) command.RunFunc {
|
||||
log := logger.FromContext(ctx)
|
||||
secretName := args[0]
|
||||
secret := &v1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Labels: map[string]string{NameLabel: secretName},
|
||||
@@ -47,10 +52,12 @@ func makeCreateRunFunc(hc *holos.Config, cfg *config) command.RunFunc {
|
||||
Data: make(secretData),
|
||||
}
|
||||
|
||||
clusterPrefix := fmt.Sprintf("%s-", *cfg.cluster)
|
||||
if !strings.HasPrefix(secretName, clusterPrefix) {
|
||||
const msg = "missing cluster name prefix"
|
||||
log.WarnContext(ctx, msg, "have", secretName, "want", clusterPrefix)
|
||||
if *cfg.cluster != "" {
|
||||
clusterPrefix := fmt.Sprintf("%s-", *cfg.cluster)
|
||||
if !strings.HasPrefix(secretName, clusterPrefix) {
|
||||
const msg = "missing cluster name prefix"
|
||||
log.WarnContext(ctx, msg, "have", secretName, "want", clusterPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range cfg.files {
|
||||
@@ -66,10 +73,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 {
|
||||
@@ -107,9 +116,7 @@ func makeWalkFunc(data secretData, root string) fs.WalkDirFunc {
|
||||
depth := strings.Count(path[len(root):], string(filepath.Separator))
|
||||
|
||||
if depth > 1 {
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
if !d.IsDir() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
@@ -25,6 +26,7 @@ func NewGetCmd(hc *holos.Config) *cobra.Command {
|
||||
flagSet.Var(&cfg.files, "to-file", "extract files from the secret")
|
||||
cfg.printFile = flagSet.String(printFlagName, "", "print one key from the secret")
|
||||
cfg.extract = flagSet.Bool("extract-all", false, "extract all files from the secret")
|
||||
cfg.extractTo = flagSet.String("extract-to", ".", "extract to directory")
|
||||
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.Flags().AddGoFlagSet(flagSet)
|
||||
@@ -61,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.
|
||||
@@ -109,10 +111,11 @@ func makeGetRunFunc(hc *holos.Config, cfg *config) command.RunFunc {
|
||||
err := fmt.Errorf("%s not found in %v", name, keys)
|
||||
return wrapper.Wrap(err)
|
||||
}
|
||||
if err := os.WriteFile(name, data, 0666); err != nil {
|
||||
return wrapper.Wrap(fmt.Errorf("could not write %s: %w", name, err))
|
||||
path := filepath.Join(*cfg.extractTo, name)
|
||||
if err := os.WriteFile(path, data, 0666); err != nil {
|
||||
return wrapper.Wrap(fmt.Errorf("could not write %s: %w", path, err))
|
||||
}
|
||||
log.InfoContext(ctx, "wrote: "+name, "bytes", len(data))
|
||||
log.InfoContext(ctx, "wrote: "+path, "name", name, "bytes", len(data))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,14 @@ 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
|
||||
files holos.StringSlice
|
||||
printFile *string
|
||||
extract *bool
|
||||
dryRun *bool
|
||||
appendHash *bool
|
||||
cluster *string
|
||||
namespace *string
|
||||
extractTo *string
|
||||
}
|
||||
|
||||
func newConfig() (*config, *flag.FlagSet) {
|
||||
|
||||
82
pkg/cli/secret/secret_test.go
Normal file
82
pkg/cli/secret/secret_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package secret_test
|
||||
|
||||
import (
|
||||
"github.com/holos-run/holos/pkg/cli"
|
||||
"github.com/holos-run/holos/pkg/holos"
|
||||
"github.com/rogpeppe/go-internal/testscript"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const clientsetKey = "clientset"
|
||||
|
||||
var secret = v1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "k2-talos",
|
||||
Namespace: "secrets",
|
||||
Labels: map[string]string{
|
||||
"holos.run/owner.name": "jeff",
|
||||
"holos.run/secret.name": "k2-talos",
|
||||
},
|
||||
CreationTimestamp: metav1.Time{
|
||||
Time: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"secrets.yaml": []byte("content: secret\n"),
|
||||
},
|
||||
Type: "Opaque",
|
||||
}
|
||||
|
||||
// cmdHolos executes the holos root command with a kubernetes.Interface that
|
||||
// persists for the duration of the testscript. holos is NOT executed in a
|
||||
// subprocess, the current working directory is not and should not be changed.
|
||||
// Take care to read and write to $WORK in the test scripts using flags.
|
||||
func cmdHolos(ts *testscript.TestScript, neg bool, args []string) {
|
||||
clientset, ok := ts.Value(clientsetKey).(kubernetes.Interface)
|
||||
if clientset == nil || !ok {
|
||||
ts.Fatalf("missing kubernetes.Interface")
|
||||
}
|
||||
|
||||
cfg := holos.New(
|
||||
holos.ProvisionerClientset(clientset),
|
||||
holos.Stdout(ts.Stdout()),
|
||||
holos.Stderr(ts.Stderr()),
|
||||
)
|
||||
|
||||
cmd := cli.New(cfg)
|
||||
cmd.SetArgs(args)
|
||||
err := cmd.Execute()
|
||||
|
||||
if neg {
|
||||
if err == nil {
|
||||
ts.Fatalf("\nwant: error\nhave: %v", err)
|
||||
} else {
|
||||
cli.HandleError(cmd.Context(), err, cfg)
|
||||
}
|
||||
} else {
|
||||
ts.Check(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecrets(t *testing.T) {
|
||||
// Add TestWork: true to the Params to keep the $WORK directory around.
|
||||
testscript.Run(t, testscript.Params{
|
||||
Dir: "testdata",
|
||||
Setup: func(env *testscript.Env) error {
|
||||
env.Values[clientsetKey] = fake.NewSimpleClientset(&secret)
|
||||
return nil
|
||||
},
|
||||
Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
|
||||
"holos": cmdHolos,
|
||||
},
|
||||
})
|
||||
}
|
||||
21
pkg/cli/secret/testdata/create_secret_dry_run.txt
vendored
Normal file
21
pkg/cli/secret/testdata/create_secret_dry_run.txt
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Create the secret
|
||||
holos create secret directory --from-file=$WORK/fixture --dry-run
|
||||
|
||||
# Want no warnings.
|
||||
! stderr 'WRN'
|
||||
|
||||
# Want the data keys
|
||||
stdout 'one.yaml: Y29udGVudDogb25lCg=='
|
||||
stdout 'two.yaml: Y29udGVudDogdHdvCg=='
|
||||
|
||||
# Want the secret name label.
|
||||
stdout 'holos.run/secret.name: directory'
|
||||
|
||||
# Want the TypeMeta
|
||||
stdout 'kind: Secret'
|
||||
stdout 'apiVersion: v1'
|
||||
|
||||
-- fixture/one.yaml --
|
||||
content: one
|
||||
-- fixture/two.yaml --
|
||||
content: two
|
||||
22
pkg/cli/secret/testdata/create_secret_from_dir.txt
vendored
Normal file
22
pkg/cli/secret/testdata/create_secret_from_dir.txt
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
# Create the secret
|
||||
holos create secret directory --from-file=$WORK/want
|
||||
stderr 'created: directory-..........'
|
||||
stderr 'secret=directory-..........'
|
||||
stderr 'name=directory'
|
||||
stderr 'namespace=secrets'
|
||||
! stderr 'WRN'
|
||||
|
||||
# Get the secret back
|
||||
mkdir have
|
||||
holos get secret directory --extract-all --extract-to=$WORK/have
|
||||
stderr 'wrote: .*/have/one.yaml'
|
||||
stderr 'wrote: .*/have/two.yaml'
|
||||
|
||||
# Compare the secrets
|
||||
cmp want/one.yaml have/one.yaml
|
||||
cmp want/two.yaml have/two.yaml
|
||||
|
||||
-- want/one.yaml --
|
||||
content: one
|
||||
-- want/two.yaml --
|
||||
content: two
|
||||
14
pkg/cli/secret/testdata/create_secret_from_file.txt
vendored
Normal file
14
pkg/cli/secret/testdata/create_secret_from_file.txt
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Create the secret.
|
||||
holos create secret k3-talos --from-file $WORK/secrets.yaml
|
||||
|
||||
# Want info log attributes.
|
||||
stderr 'created: k3-talos-..........'
|
||||
stderr 'secret=k3-talos-..........'
|
||||
stderr 'name=k3-talos'
|
||||
stderr 'namespace=secrets'
|
||||
|
||||
# Want no warnings.
|
||||
! stderr 'WRN'
|
||||
|
||||
-- secrets.yaml --
|
||||
content: hello
|
||||
14
pkg/cli/secret/testdata/create_secret_namespace.txt
vendored
Normal file
14
pkg/cli/secret/testdata/create_secret_namespace.txt
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Create the secret.
|
||||
holos create secret k3-talos --namespace=jeff --from-file $WORK/secrets.yaml
|
||||
stderr 'created: k3-talos-..........'
|
||||
stderr 'secret=k3-talos-..........'
|
||||
stderr 'name=k3-talos'
|
||||
|
||||
# Want specified namespace.
|
||||
stderr 'namespace=jeff'
|
||||
|
||||
# Want no warnings.
|
||||
! stderr 'WRN'
|
||||
|
||||
-- secrets.yaml --
|
||||
content: hello
|
||||
24
pkg/cli/secret/testdata/create_secret_no_depth.txt
vendored
Normal file
24
pkg/cli/secret/testdata/create_secret_no_depth.txt
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Create the secret
|
||||
holos create secret directory --from-file=$WORK/want
|
||||
|
||||
# Get the secret back
|
||||
mkdir have
|
||||
holos get secret directory --extract-all --extract-to=$WORK/have
|
||||
stderr 'wrote: .*/have/one.yaml'
|
||||
stderr 'wrote: .*/have/two.yaml'
|
||||
! stderr 'wrote: .*omit.yaml'
|
||||
|
||||
# Compare the secrets
|
||||
cmp want/one.yaml have/one.yaml
|
||||
cmp want/two.yaml have/two.yaml
|
||||
|
||||
# Want no files with depth > 1
|
||||
! exists have/nope/omit.yaml
|
||||
! exists have/omit.yaml
|
||||
|
||||
-- want/one.yaml --
|
||||
content: one
|
||||
-- want/two.yaml --
|
||||
content: two
|
||||
-- want/nope/omit.yaml --
|
||||
content: not included
|
||||
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
|
||||
11
pkg/cli/secret/testdata/create_secret_warns.txt
vendored
Normal file
11
pkg/cli/secret/testdata/create_secret_warns.txt
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Create the secret.
|
||||
holos create secret k3-talos --cluster-name=k2 --from-file $WORK/secrets.yaml
|
||||
stderr 'created: k3-talos-..........'
|
||||
|
||||
# Want a warning about the cluster name prefix.
|
||||
stderr 'missing cluster name prefix'
|
||||
stderr 'have=k3-talos'
|
||||
stderr 'want=k2-'
|
||||
|
||||
-- secrets.yaml --
|
||||
content: hello
|
||||
10
pkg/cli/secret/testdata/get_secret_extract_all.txt
vendored
Normal file
10
pkg/cli/secret/testdata/get_secret_extract_all.txt
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Get and extract the secret
|
||||
holos get secrets k2-talos --extract-all --extract-to=$WORK
|
||||
! stdout .
|
||||
stderr 'wrote: .*/secrets\.yaml'
|
||||
|
||||
# Check the secret keys
|
||||
cmp want.secrets.yaml secrets.yaml
|
||||
|
||||
-- want.secrets.yaml --
|
||||
content: secret
|
||||
3
pkg/cli/secret/testdata/get_secret_print.txt
vendored
Normal file
3
pkg/cli/secret/testdata/get_secret_print.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
holos get secrets k2-talos --print-key=secrets.yaml
|
||||
stdout -count=1 '^content: secret$'
|
||||
! stderr .
|
||||
3
pkg/cli/secret/testdata/get_secrets.txt
vendored
Normal file
3
pkg/cli/secret/testdata/get_secrets.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
holos get secrets
|
||||
stdout '^k2-talos$'
|
||||
! stderr .
|
||||
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
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@ const DefaultProvisionerNamespace = "secrets"
|
||||
type Option func(o *options)
|
||||
|
||||
type options struct {
|
||||
stdin io.Reader
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
stdin io.Reader
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
provisionerClientset kubernetes.Interface
|
||||
clientset kubernetes.Interface
|
||||
}
|
||||
|
||||
// Stdin redirects standard input to r, useful for test capture.
|
||||
@@ -41,6 +43,16 @@ func Stderr(w io.Writer) Option {
|
||||
return func(o *options) { o.stderr = w }
|
||||
}
|
||||
|
||||
// ProvisionerClientset sets the kubernetes Clientset, useful for test fake.
|
||||
func ProvisionerClientset(clientset kubernetes.Interface) Option {
|
||||
return func(o *options) { o.provisionerClientset = clientset }
|
||||
}
|
||||
|
||||
// ClusterClientset sets the kubernetes Clientset, useful for test fake.
|
||||
func ClusterClientset(clientset *kubernetes.Clientset) Option {
|
||||
return func(o *options) { o.clientset = clientset }
|
||||
}
|
||||
|
||||
// New returns a new top level cli Config.
|
||||
func New(opts ...Option) *Config {
|
||||
cfgOptions := &options{
|
||||
@@ -56,14 +68,15 @@ func New(opts ...Option) *Config {
|
||||
kvFlagSet := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
txFlagSet := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
cfg := &Config{
|
||||
logConfig: logger.NewConfig(),
|
||||
writeTo: getenv("HOLOS_WRITE_TO", "deploy"),
|
||||
clusterName: getenv("HOLOS_CLUSTER_NAME", ""),
|
||||
writeFlagSet: writeFlagSet,
|
||||
clusterFlagSet: clusterFlagSet,
|
||||
options: cfgOptions,
|
||||
kvFlagSet: kvFlagSet,
|
||||
txtarFlagSet: txFlagSet,
|
||||
logConfig: logger.NewConfig(),
|
||||
writeTo: getenv("HOLOS_WRITE_TO", "deploy"),
|
||||
clusterName: getenv("HOLOS_CLUSTER_NAME", ""),
|
||||
writeFlagSet: writeFlagSet,
|
||||
clusterFlagSet: clusterFlagSet,
|
||||
options: cfgOptions,
|
||||
kvFlagSet: kvFlagSet,
|
||||
txtarFlagSet: txFlagSet,
|
||||
provisionerClientset: cfgOptions.provisionerClientset,
|
||||
}
|
||||
writeFlagSet.StringVar(&cfg.writeTo, "write-to", cfg.writeTo, "write to directory")
|
||||
clusterFlagSet.StringVar(&cfg.clusterName, "cluster-name", cfg.clusterName, "cluster name")
|
||||
@@ -96,7 +109,7 @@ type Config struct {
|
||||
kvFlagSet *flag.FlagSet
|
||||
txtarIndex *int
|
||||
txtarFlagSet *flag.FlagSet
|
||||
provisionerClientset *kubernetes.Clientset
|
||||
provisionerClientset kubernetes.Interface
|
||||
}
|
||||
|
||||
// LogFlagSet returns the logging *flag.FlagSet for use by the command handler.
|
||||
@@ -229,7 +242,7 @@ func (c *Config) TxtarIndex() int {
|
||||
}
|
||||
|
||||
// ProvisionerClientset returns a kubernetes client set for the provisioner cluster.
|
||||
func (c *Config) ProvisionerClientset() (*kubernetes.Clientset, error) {
|
||||
func (c *Config) ProvisionerClientset() (kubernetes.Interface, error) {
|
||||
if c.provisionerClientset == nil {
|
||||
kcfg, err := clientcmd.BuildConfigFromFlags("", c.KVKubeconfig())
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ 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"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -74,9 +75,10 @@ type Metadata struct {
|
||||
|
||||
// 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"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ContentType string `json:"contentType"`
|
||||
KsContent string `json:"ksContent,omitempty"`
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
@@ -99,6 +101,8 @@ type HelmChart struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Chart Chart `json:"chart"`
|
||||
ValuesContent string `json:"valuesContent"`
|
||||
ContentType string `json:"contentType"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
// Name returns the metadata name of the result. Equivalent to the
|
||||
@@ -225,6 +229,16 @@ 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)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, wrapper.Wrap(fmt.Errorf("build kind not implemented: %v", kind))
|
||||
}
|
||||
|
||||
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
|
||||
47
|
||||
|
||||
@@ -1 +1 @@
|
||||
1
|
||||
0
|
||||
|
||||
Reference in New Issue
Block a user