mirror of
https://github.com/holos-run/holos.git
synced 2026-03-19 08:44:58 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
340715f76c | ||
|
|
64ffacfc7a | ||
|
|
54acea42cb | ||
|
|
5ef8e75194 | ||
|
|
cb2b5c0f49 | ||
|
|
fd5a2fdbc1 | ||
|
|
eb3e272612 | ||
|
|
9f2a51bde8 | ||
|
|
2b3b5a4887 | ||
|
|
7426e8f867 | ||
|
|
cf0c455aa2 |
5
.github/workflows/lint.yaml
vendored
5
.github/workflows/lint.yaml
vendored
@@ -1,6 +1,7 @@
|
||||
---
|
||||
# https://github.com/golangci/golangci-lint-action?tab=readme-ov-file#how-to-use
|
||||
name: Lint
|
||||
on:
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -22,6 +23,6 @@ jobs:
|
||||
go-version: stable
|
||||
cache: false
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
version: latest
|
||||
|
||||
61
docs/examples/platforms/reference/certificates.cue
Normal file
61
docs/examples/platforms/reference/certificates.cue
Normal file
@@ -0,0 +1,61 @@
|
||||
package holos
|
||||
|
||||
#PlatformCerts: {
|
||||
// Globally scoped platform services are defined here.
|
||||
login: #PlatformCert & {
|
||||
_name: "login"
|
||||
_wildcard: true
|
||||
_description: "Cert for Zitadel oidc identity provider for iam services"
|
||||
}
|
||||
|
||||
// Cluster scoped services are defined here.
|
||||
for cluster in #Platform.clusters {
|
||||
"\(cluster.name)-httpbin": #ClusterCert & {
|
||||
_name: "httpbin"
|
||||
_cluster: cluster.name
|
||||
_description: "Test endpoint to verify the service mesh ingress gateway"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #PlatformCert provisions a cert in the provisioner cluster.
|
||||
// Workload clusters use ExternalSecret resources to fetch the Secret tls key and cert from the provisioner cluster.
|
||||
#PlatformCert: #Certificate & {
|
||||
_name: string
|
||||
_wildcard: true | *false
|
||||
metadata: name: string | *_name
|
||||
metadata: namespace: string | *"istio-ingress"
|
||||
spec: {
|
||||
commonName: string | *"\(_name).\(#Platform.org.domain)"
|
||||
if _wildcard {
|
||||
dnsNames: [commonName, "*.\(commonName)"]
|
||||
}
|
||||
if !_wildcard {
|
||||
dnsNames: [commonName]
|
||||
}
|
||||
secretName: metadata.name
|
||||
issuerRef: kind: "ClusterIssuer"
|
||||
issuerRef: name: string | *"letsencrypt"
|
||||
}
|
||||
}
|
||||
|
||||
// #ClusterCert provisions a cluster specific certificate.
|
||||
#ClusterCert: #Certificate & {
|
||||
_name: string
|
||||
_cluster: string
|
||||
_wildcard: true | *false
|
||||
metadata: name: string | *"\(_cluster)-\(_name)"
|
||||
metadata: namespace: string | *"istio-ingress"
|
||||
spec: {
|
||||
commonName: string | *"\(_name).\(_cluster).\(#Platform.org.domain)"
|
||||
if _wildcard {
|
||||
dnsNames: [commonName, "*.\(commonName)"]
|
||||
}
|
||||
if !_wildcard {
|
||||
dnsNames: [commonName]
|
||||
}
|
||||
secretName: metadata.name
|
||||
issuerRef: kind: "ClusterIssuer"
|
||||
issuerRef: name: string | *"letsencrypt"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package holos
|
||||
|
||||
// Components under this directory are part of this collection
|
||||
#InputKeys: project: "iam"
|
||||
|
||||
// Shared dependencies for all components in this collection.
|
||||
#DependsOn: _Namespaces
|
||||
|
||||
// Common Dependencies
|
||||
_Namespaces: Namespaces: name: "\(#StageName)-secrets-namespaces"
|
||||
@@ -0,0 +1,108 @@
|
||||
package holos
|
||||
|
||||
// Manage an Issuer for cockroachdb for zitadel.
|
||||
// For the iam login service, zitadel connects to cockroach db using tls certs for authz.
|
||||
// Upstream: "The recommended approach is to use cert-manager for certificate management. For details, refer to Deploy cert-manager for mTLS."
|
||||
// Refer to https://www.cockroachlabs.com/docs/stable/secure-cockroachdb-kubernetes#deploy-cert-manager-for-mtls
|
||||
|
||||
#InputKeys: component: "crdb"
|
||||
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
Issuer: {
|
||||
// https://github.com/cockroachdb/helm-charts/blob/3dcf96726ebcfe3784afb526ddcf4095a1684aea/README.md?plain=1#L196-L201
|
||||
crdb: #Issuer & {
|
||||
_description: "Issues the self signed root ca cert for cockroach db"
|
||||
metadata: name: #ComponentName
|
||||
metadata: namespace: #TargetNamespace
|
||||
spec: selfSigned: {}
|
||||
}
|
||||
"crdb-ca-issuer": #Issuer & {
|
||||
_description: "Issues mtls certs for cockroach db"
|
||||
metadata: name: "crdb-ca-issuer"
|
||||
metadata: namespace: #TargetNamespace
|
||||
spec: ca: secretName: "cockroach-ca"
|
||||
}
|
||||
}
|
||||
Certificate: {
|
||||
"crdb-ca-cert": #Certificate & {
|
||||
_description: "Root CA cert for cockroach db"
|
||||
metadata: name: "crdb-ca-cert"
|
||||
metadata: namespace: #TargetNamespace
|
||||
spec: {
|
||||
commonName: "root"
|
||||
isCA: true
|
||||
issuerRef: group: "cert-manager.io"
|
||||
issuerRef: kind: "Issuer"
|
||||
issuerRef: name: "crdb"
|
||||
privateKey: algorithm: "ECDSA"
|
||||
privateKey: size: 256
|
||||
secretName: "cockroach-ca"
|
||||
subject: organizations: ["Cockroach"]
|
||||
}
|
||||
}
|
||||
"crdb-node": #Certificate & {
|
||||
metadata: name: "crdb-node"
|
||||
metadata: namespace: #TargetNamespace
|
||||
spec: {
|
||||
commonName: "node"
|
||||
dnsNames: [
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
"crdb-public",
|
||||
"crdb-public.\(#TargetNamespace)",
|
||||
"crdb-public.\(#TargetNamespace).svc.cluster.local",
|
||||
"*.crdb",
|
||||
"*.crdb.\(#TargetNamespace)",
|
||||
"*.crdb.\(#TargetNamespace).svc.cluster.local",
|
||||
]
|
||||
duration: "876h"
|
||||
issuerRef: group: "cert-manager.io"
|
||||
issuerRef: kind: "Issuer"
|
||||
issuerRef: name: "crdb-ca-issuer"
|
||||
privateKey: algorithm: "RSA"
|
||||
privateKey: size: 2048
|
||||
renewBefore: "168h"
|
||||
secretName: "cockroachdb-node"
|
||||
subject: organizations: ["Cockroach"]
|
||||
usages: ["digital signature", "key encipherment", "server auth", "client auth"]
|
||||
}
|
||||
}
|
||||
"crdb-root-client": #Certificate & {
|
||||
metadata: name: "crdb-root-client"
|
||||
metadata: namespace: #TargetNamespace
|
||||
spec: {
|
||||
commonName: "root"
|
||||
duration: "672h"
|
||||
issuerRef: group: "cert-manager.io"
|
||||
issuerRef: kind: "Issuer"
|
||||
issuerRef: name: "crdb-ca-issuer"
|
||||
privateKey: algorithm: "RSA"
|
||||
privateKey: size: 2048
|
||||
renewBefore: "48h"
|
||||
secretName: "cockroachdb-root"
|
||||
subject: organizations: ["Cockroach"]
|
||||
usages: ["digital signature", "key encipherment", "client auth"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Certificate: zitadel: #Certificate & {
|
||||
metadata: name: "crdb-zitadel-client"
|
||||
metadata: namespace: #TargetNamespace
|
||||
spec: {
|
||||
commonName: "zitadel"
|
||||
issuerRef: {
|
||||
group: "cert-manager.io"
|
||||
kind: "Issuer"
|
||||
name: "crdb-ca-issuer"
|
||||
}
|
||||
privateKey: algorithm: "RSA"
|
||||
privateKey: size: 2048
|
||||
renewBefore: "48h0m0s"
|
||||
secretName: "cockroachdb-zitadel"
|
||||
subject: organizations: ["Cockroach"]
|
||||
usages: ["digital signature", "key encipherment", "client auth"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package holos
|
||||
|
||||
#TargetNamespace: #InstancePrefix + "-zitadel"
|
||||
|
||||
#DB: {
|
||||
Host: "crdb-public"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package holos
|
||||
|
||||
// Provision all platform certificates.
|
||||
#InputKeys: component: "certificates"
|
||||
|
||||
// Certificates usually go into the istio-system namespace, but they may go anywhere.
|
||||
#TargetNamespace: "default"
|
||||
|
||||
// Depends on issuers
|
||||
#DependsOn: _LetsEncrypt
|
||||
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
for k, obj in #PlatformCerts {
|
||||
"\(obj.kind)": {
|
||||
"\(obj.metadata.namespace)/\(obj.metadata.name)": obj
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package holos
|
||||
|
||||
// https://cert-manager.io/docs/
|
||||
|
||||
#TargetNamespace: "cert-manager"
|
||||
|
||||
#InputKeys: {
|
||||
component: "certmanager"
|
||||
service: "cert-manager"
|
||||
}
|
||||
|
||||
#HelmChart & {
|
||||
values: #Values & {
|
||||
installCRDs: true
|
||||
startupapicheck: enabled: false
|
||||
// Must not use kube-system on gke autopilot. GKE Warden authz blocks access.
|
||||
global: leaderElection: namespace: #TargetNamespace
|
||||
}
|
||||
namespace: #TargetNamespace
|
||||
chart: {
|
||||
name: "cert-manager"
|
||||
version: "1.14.3"
|
||||
repository: {
|
||||
name: "jetstack"
|
||||
url: "https://charts.jetstack.io"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://cloud.google.com/kubernetes-engine/docs/concepts/autopilot-resource-requests#min-max-requests
|
||||
#PodResources: {
|
||||
requests: {
|
||||
cpu: string | *"250m"
|
||||
memory: string | *"512Mi"
|
||||
"ephemeral-storage": string | *"100Mi"
|
||||
}
|
||||
}
|
||||
|
||||
// https://cloud.google.com/kubernetes-engine/docs/how-to/autopilot-spot-pods
|
||||
#NodeSelector: {
|
||||
"kubernetes.io/os": "linux"
|
||||
"cloud.google.com/gke-spot": "true"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package holos
|
||||
|
||||
#UpstreamValues: {
|
||||
#Values: {
|
||||
|
||||
// +docs:section=Global
|
||||
// Default values for cert-manager.
|
||||
@@ -51,7 +51,7 @@ package holos
|
||||
|
||||
leaderElection: {
|
||||
// Override the namespace used for the leader election lease
|
||||
namespace: "kube-system"
|
||||
namespace: string | *"kube-system"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ package holos
|
||||
// memory: 32Mi
|
||||
//
|
||||
// ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
resources: {}
|
||||
resources: #PodResources
|
||||
|
||||
// Pod Security Context
|
||||
// ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
||||
@@ -310,9 +310,7 @@ package holos
|
||||
// This default ensures that Pods are only scheduled to Linux nodes.
|
||||
// It prevents Pods being scheduled to Windows nodes in a mixed OS cluster.
|
||||
// +docs:property
|
||||
nodeSelector: {
|
||||
"kubernetes.io/os": "linux"
|
||||
}
|
||||
nodeSelector: #NodeSelector
|
||||
|
||||
// +docs:ignore
|
||||
ingressShim: {}
|
||||
@@ -408,7 +406,7 @@ package holos
|
||||
enabled: true
|
||||
servicemonitor: {
|
||||
// Create a ServiceMonitor to add cert-manager to Prometheus
|
||||
enabled: false
|
||||
enabled: true | *false
|
||||
|
||||
// Specifies the `prometheus` label on the created ServiceMonitor, this is
|
||||
// used when different Prometheus instances have label selectors matching
|
||||
@@ -652,7 +650,7 @@ package holos
|
||||
// memory: 32Mi
|
||||
//
|
||||
// ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
resources: {}
|
||||
resources: #PodResources
|
||||
|
||||
// Liveness probe values
|
||||
// ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes
|
||||
@@ -685,9 +683,7 @@ package holos
|
||||
// This default ensures that Pods are only scheduled to Linux nodes.
|
||||
// It prevents Pods being scheduled to Windows nodes in a mixed OS cluster.
|
||||
// +docs:property
|
||||
nodeSelector: {
|
||||
"kubernetes.io/os": "linux"
|
||||
}
|
||||
nodeSelector: #NodeSelector
|
||||
|
||||
// A Kubernetes Affinity, if required; see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#affinity-v1-core
|
||||
//
|
||||
@@ -959,7 +955,7 @@ package holos
|
||||
// memory: 32Mi
|
||||
//
|
||||
// ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
resources: {}
|
||||
resources: #PodResources
|
||||
|
||||
// The nodeSelector on Pods tells Kubernetes to schedule Pods on the nodes with
|
||||
// matching labels.
|
||||
@@ -968,9 +964,7 @@ package holos
|
||||
// This default ensures that Pods are only scheduled to Linux nodes.
|
||||
// It prevents Pods being scheduled to Windows nodes in a mixed OS cluster.
|
||||
// +docs:property
|
||||
nodeSelector: {
|
||||
"kubernetes.io/os": "linux"
|
||||
}
|
||||
nodeSelector: #NodeSelector
|
||||
|
||||
// A Kubernetes Affinity, if required; see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#affinity-v1-core
|
||||
//
|
||||
@@ -1098,7 +1092,7 @@ package holos
|
||||
|
||||
startupapicheck: {
|
||||
// Enables the startup api check
|
||||
enabled: true
|
||||
enabled: *true | false
|
||||
|
||||
// Pod Security Context to be set on the startupapicheck component Pod
|
||||
// ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
|
||||
@@ -1151,7 +1145,7 @@ package holos
|
||||
// memory: 32Mi
|
||||
//
|
||||
// ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||
resources: {}
|
||||
resources: #PodResources
|
||||
|
||||
// The nodeSelector on Pods tells Kubernetes to schedule Pods on the nodes with
|
||||
// matching labels.
|
||||
@@ -1160,9 +1154,7 @@ package holos
|
||||
// This default ensures that Pods are only scheduled to Linux nodes.
|
||||
// It prevents Pods being scheduled to Windows nodes in a mixed OS cluster.
|
||||
// +docs:property
|
||||
nodeSelector: {
|
||||
"kubernetes.io/os": "linux"
|
||||
}
|
||||
nodeSelector: #NodeSelector
|
||||
|
||||
// A Kubernetes Affinity, if required; see https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#affinity-v1-core
|
||||
//
|
||||
@@ -0,0 +1,78 @@
|
||||
package holos
|
||||
|
||||
// Lets Encrypt certificate issuers for public tls certs
|
||||
#InputKeys: component: "letsencrypt"
|
||||
#TargetNamespace: "cert-manager"
|
||||
|
||||
let Name = "letsencrypt"
|
||||
|
||||
// The cloudflare api token is platform scoped, not cluster scoped.
|
||||
#SecretName: "cloudflare-api-token-secret"
|
||||
|
||||
// Depends on cert manager
|
||||
#DependsOn: _CertManager
|
||||
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
ClusterIssuer: {
|
||||
letsencrypt: #ClusterIssuer & {
|
||||
metadata: name: Name
|
||||
spec: {
|
||||
acme: {
|
||||
email: #Platform.org.contact.email
|
||||
server: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
privateKeySecretRef: name: Name
|
||||
solvers: [{
|
||||
dns01: cloudflare: {
|
||||
email: #Platform.org.cloudflare.email
|
||||
apiTokenSecretRef: name: #SecretName
|
||||
apiTokenSecretRef: key: "api_token"
|
||||
}}]
|
||||
}
|
||||
}
|
||||
}
|
||||
letsencryptStaging: #ClusterIssuer & {
|
||||
metadata: name: Name + "-staging"
|
||||
spec: {
|
||||
acme: {
|
||||
email: #Platform.org.contact.email
|
||||
server: "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
privateKeySecretRef: name: Name + "-staging"
|
||||
solvers: [{
|
||||
dns01: cloudflare: {
|
||||
email: #Platform.org.cloudflare.email
|
||||
apiTokenSecretRef: name: #SecretName
|
||||
apiTokenSecretRef: key: "api_token"
|
||||
}}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// _HTTPSolvers are disabled in the provisioner cluster, dns is the method supported by holos.
|
||||
_HTTPSolvers: {
|
||||
letsencryptHTTP: #ClusterIssuer & {
|
||||
metadata: name: Name + "-http"
|
||||
spec: {
|
||||
acme: {
|
||||
email: #Platform.org.contact.email
|
||||
server: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
privateKeySecretRef: name: Name
|
||||
solvers: [{http01: ingress: class: "istio"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
letsencryptHTTPStaging: #ClusterIssuer & {
|
||||
metadata: name: Name + "-http-staging"
|
||||
spec: {
|
||||
acme: {
|
||||
email: #Platform.org.contact.email
|
||||
server: "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
privateKeySecretRef: name: Name + "-staging"
|
||||
solvers: [{http01: ingress: class: "istio"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package holos
|
||||
|
||||
// Components under this directory are part of this collection
|
||||
#InputKeys: project: "mesh"
|
||||
|
||||
// Shared dependencies for all components in this collection.
|
||||
#DependsOn: _Namespaces
|
||||
|
||||
// Common Dependencies
|
||||
_Namespaces: Namespaces: name: "\(#StageName)-secrets-namespaces"
|
||||
_CertManager: CertManager: name: "\(#InstancePrefix)-certmanager"
|
||||
_LetsEncrypt: LetsEncrypt: name: "\(#InstancePrefix)-letsencrypt"
|
||||
_Certificates: Certificates: name: "\(#InstancePrefix)-certificates"
|
||||
@@ -14,13 +14,7 @@ package holos
|
||||
}
|
||||
values: #Values
|
||||
apiObjects: {
|
||||
Issuer: {
|
||||
// https://github.com/cockroachdb/helm-charts/blob/3dcf96726ebcfe3784afb526ddcf4095a1684aea/README.md?plain=1#L196-L201
|
||||
cockroachdb: #Issuer & {
|
||||
metadata: name: #ComponentName
|
||||
metadata: namespace: #TargetNamespace
|
||||
spec: selfSigned: {}
|
||||
}
|
||||
}
|
||||
ExternalSecret: node: #ExternalSecret & {_name: "cockroachdb-node"}
|
||||
ExternalSecret: root: #ExternalSecret & {_name: "cockroachdb-root"}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ package holos
|
||||
copyCerts: image: "busybox"
|
||||
certs: {
|
||||
// Bring your own certs scenario. If provided, tls.init section will be ignored.
|
||||
provided: false
|
||||
provided: true | *false
|
||||
// Secret name for the client root cert.
|
||||
clientRootSecret: "cockroachdb-root"
|
||||
// Secret name for node cert.
|
||||
@@ -487,7 +487,7 @@ package holos
|
||||
caSecret: "cockroach-ca"
|
||||
// Enable if the secret is a dedicated TLS.
|
||||
// TLS secrets are created by cert-mananger, for example.
|
||||
tlsSecret: false
|
||||
tlsSecret: true | *false
|
||||
// Enable if the you want cockroach db to create its own certificates
|
||||
selfSigner: {
|
||||
// If set, the cockroach db will generate its own certificates
|
||||
|
||||
@@ -10,11 +10,9 @@ package holos
|
||||
certs: {
|
||||
// https://github.com/cockroachdb/helm-charts/blob/3dcf96726ebcfe3784afb526ddcf4095a1684aea/README.md?plain=1#L204-L215
|
||||
selfSigner: enabled: false
|
||||
certManager: true
|
||||
certManagerIssuer: {
|
||||
kind: "Issuer"
|
||||
name: #ComponentName
|
||||
}
|
||||
certManager: false
|
||||
provided: true
|
||||
tlsSecret: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,23 +24,8 @@ let Name = "zitadel"
|
||||
ExternalSecret: masterkey: #ExternalSecret & {
|
||||
_name: "zitadel-masterkey"
|
||||
}
|
||||
Certificate: zitadel: #Certificate & {
|
||||
metadata: name: "crdb-zitadel-client"
|
||||
metadata: namespace: #TargetNamespace
|
||||
spec: {
|
||||
commonName: "zitadel"
|
||||
issuerRef: {
|
||||
group: "cert-manager.io"
|
||||
kind: "Issuer"
|
||||
name: "crdb-ca-issuer"
|
||||
}
|
||||
privateKey: algorithm: "RSA"
|
||||
privateKey: size: 2048
|
||||
renewBefore: "48h0m0s"
|
||||
secretName: "cockroachdb-zitadel"
|
||||
subject: organizations: ["Cockroach"]
|
||||
usages: ["digital signature", "key encipherment", "client auth"]
|
||||
}
|
||||
ExternalSecret: zitadel: #ExternalSecret & {
|
||||
_name: "cockroachdb-zitadel"
|
||||
}
|
||||
VirtualService: zitadel: #VirtualService & {
|
||||
metadata: name: Name
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
package holos
|
||||
|
||||
// Lets Encrypt certificate issuers for public tls certs
|
||||
#InputKeys: component: "certissuers"
|
||||
#TargetNamespace: "cert-manager"
|
||||
|
||||
let Name = "letsencrypt"
|
||||
|
||||
// The cloudflare api token is platform scoped, not cluster scoped.
|
||||
#SecretName: "cloudflare-api-token-secret"
|
||||
|
||||
// Depends on cert manager
|
||||
#DependsOn: _CertManager
|
||||
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
ClusterIssuer: {
|
||||
letsencrypt: #ClusterIssuer & {
|
||||
metadata: name: Name
|
||||
spec: {
|
||||
acme: {
|
||||
email: #Platform.org.contact.email
|
||||
server: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
privateKeySecretRef: name: Name + "-istio"
|
||||
solvers: [{http01: ingress: class: "istio"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
letsencryptStaging: #ClusterIssuer & {
|
||||
metadata: name: Name + "-staging"
|
||||
spec: {
|
||||
acme: {
|
||||
email: #Platform.org.contact.email
|
||||
server: "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
privateKeySecretRef: name: Name + "-staging-istio"
|
||||
solvers: [{http01: ingress: class: "istio"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
letsencryptDns: #ClusterIssuer & {
|
||||
metadata: name: Name + "-dns"
|
||||
spec: {
|
||||
acme: {
|
||||
email: #Platform.org.contact.email
|
||||
server: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
privateKeySecretRef: name: Name + "-istio"
|
||||
solvers: [{
|
||||
dns01: cloudflare: {
|
||||
email: #Platform.org.cloudflare.email
|
||||
apiTokenSecretRef: name: #SecretName
|
||||
apiTokenSecretRef: key: "api_token"
|
||||
}}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ExternalSecret: "\(#SecretName)": #ExternalSecret & {
|
||||
_name: #SecretName
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package holos
|
||||
|
||||
// https://cert-manager.io/docs/
|
||||
|
||||
#TargetNamespace: "cert-manager"
|
||||
|
||||
#InputKeys: {
|
||||
component: "certmanager"
|
||||
service: "cert-manager"
|
||||
}
|
||||
|
||||
#HelmChart & {
|
||||
values: #UpstreamValues & {
|
||||
installCRDs: true
|
||||
}
|
||||
namespace: #TargetNamespace
|
||||
chart: {
|
||||
name: "cert-manager"
|
||||
version: "1.14.3"
|
||||
repository: {
|
||||
name: "jetstack"
|
||||
url: "https://charts.jetstack.io"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,32 +9,21 @@ let Name = "gateway"
|
||||
#TargetNamespace: "istio-ingress"
|
||||
#DependsOn: _IngressGateway
|
||||
|
||||
// TODO: We need to generalize this for multiple services hanging off the default gateway.
|
||||
let LoginCert = #Certificate & {
|
||||
metadata: {
|
||||
name: "login"
|
||||
namespace: #TargetNamespace
|
||||
}
|
||||
spec: {
|
||||
commonName: "login.\(#Platform.org.domain)"
|
||||
dnsNames: [commonName]
|
||||
secretName: metadata.name
|
||||
issuerRef: kind: "ClusterIssuer"
|
||||
issuerRef: name: "letsencrypt"
|
||||
}
|
||||
}
|
||||
let LoginCert = #PlatformCerts.login
|
||||
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
Certificate: login: LoginCert
|
||||
ExternalSecret: login: #ExternalSecret & {
|
||||
_name: "login"
|
||||
}
|
||||
Gateway: default: #Gateway & {
|
||||
metadata: name: "default"
|
||||
metadata: namespace: #TargetNamespace
|
||||
spec: selector: istio: "ingressgateway"
|
||||
spec: servers: [
|
||||
{
|
||||
hosts: ["prod-iam-zitadel/\(LoginCert.spec.commonName)"]
|
||||
port: name: "https-prod-iam-zitadel"
|
||||
hosts: [for dnsName in LoginCert.spec.dnsNames {"prod-iam-zitadel/\(dnsName)"}]
|
||||
port: name: "https-prod-iam-login"
|
||||
port: number: 443
|
||||
port: protocol: "HTTPS"
|
||||
tls: credentialName: LoginCert.spec.secretName
|
||||
|
||||
@@ -14,14 +14,13 @@ let Metadata = {
|
||||
#TargetNamespace: "istio-ingress"
|
||||
#DependsOn: _IngressGateway
|
||||
|
||||
let Cert = #HTTP01Cert & {
|
||||
_name: Name
|
||||
_secret: SecretName
|
||||
}
|
||||
let Cert = #PlatformCerts[SecretName]
|
||||
|
||||
#KubernetesObjects & {
|
||||
apiObjects: {
|
||||
Certificate: httpbin: Cert.object
|
||||
ExternalSecret: httpbin: #ExternalSecret & {
|
||||
_name: Cert.spec.secretName
|
||||
}
|
||||
Deployment: httpbin: #Deployment & {
|
||||
metadata: Metadata
|
||||
spec: selector: matchLabels: MatchLabels
|
||||
@@ -56,18 +55,18 @@ let Cert = #HTTP01Cert & {
|
||||
spec: selector: istio: "ingressgateway"
|
||||
spec: servers: [
|
||||
{
|
||||
hosts: ["\(#TargetNamespace)/\(Cert.Host)"]
|
||||
hosts: [for host in Cert.spec.dnsNames {"\(#TargetNamespace)/\(host)"}]
|
||||
port: name: "https-\(#InstanceName)"
|
||||
port: number: 443
|
||||
port: protocol: "HTTPS"
|
||||
tls: credentialName: Cert.SecretName
|
||||
tls: credentialName: Cert.spec.secretName
|
||||
tls: mode: "SIMPLE"
|
||||
},
|
||||
]
|
||||
}
|
||||
VirtualService: httpbin: #VirtualService & {
|
||||
metadata: Metadata
|
||||
spec: hosts: [Cert.Host]
|
||||
spec: hosts: [for host in Cert.spec.dnsNames {host}]
|
||||
spec: gateways: ["\(#TargetNamespace)/\(Name)"]
|
||||
spec: http: [{route: [{destination: host: Name}]}]
|
||||
}
|
||||
|
||||
129
pkg/cli/preflight/gh.go
Normal file
129
pkg/cli/preflight/gh.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package preflight
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/holos-run/holos/pkg/logger"
|
||||
"github.com/holos-run/holos/pkg/util"
|
||||
"github.com/holos-run/holos/pkg/wrapper"
|
||||
)
|
||||
|
||||
type ghAuthStatusResponse string
|
||||
|
||||
// RunGhChecks runs all the preflight checks related to GitHub.
|
||||
func RunGhChecks(ctx context.Context, cfg *config) error {
|
||||
if err := cliIsInstalled(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cliIsAuthed(ctx, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cliIsInstalled checks if the GitHub CLI is installed.
|
||||
func cliIsInstalled(ctx context.Context) error {
|
||||
log := logger.FromContext(ctx)
|
||||
|
||||
version, err := getGhVersion(ctx)
|
||||
if err != nil {
|
||||
log.WarnContext(ctx, "GitHub CLI (gh) not installed or not in PATH.")
|
||||
return guideToInstallGh(ctx)
|
||||
}
|
||||
|
||||
log.InfoContext(ctx, "GitHub CLI found", "gh_version", version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// cliIsAuthed checks if 'gh' is authenticated. If not, 'gh auth login' is run then cliIsAuthed is called again.
|
||||
func cliIsAuthed(ctx context.Context, cfg *config) error {
|
||||
log := logger.FromContext(ctx)
|
||||
|
||||
status, err := ghAuthStatus(ctx, cfg)
|
||||
|
||||
if err != nil || !ghIsAuthenticated(status, cfg) {
|
||||
log.WarnContext(ctx, "GitHub CLI not authenticated to "+*cfg.githubInstance)
|
||||
if err := authenticateGh(ctx, cfg); err != nil {
|
||||
return wrapper.Wrap(fmt.Errorf("failed to authenticate the GitHub CLI to %v: %w", cfg.githubInstance, err))
|
||||
}
|
||||
|
||||
// Re-run this check now that gh should be authenticated.
|
||||
err := cliIsAuthed(ctx, cfg)
|
||||
return err
|
||||
}
|
||||
|
||||
log.InfoContext(ctx, "GitHub CLI is authenticated to "+*cfg.githubInstance)
|
||||
|
||||
if !ghTokenAllowsRepoCreation(status) {
|
||||
return wrapper.Wrap(fmt.Errorf("GitHub token does not have the necessary scopes to create a repository"))
|
||||
}
|
||||
|
||||
log.InfoContext(ctx, "GitHub token is able to create a repository")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ghAuthStatus runs 'gh auth status' and returns the result.
|
||||
func ghAuthStatus(ctx context.Context, cfg *config) (ghAuthStatusResponse, error) {
|
||||
log := logger.FromContext(ctx)
|
||||
out, err := util.RunCmd(ctx, "gh", "auth", "status", "--hostname="+*cfg.githubInstance)
|
||||
|
||||
var status ghAuthStatusResponse
|
||||
if err != nil {
|
||||
status = ghAuthStatusResponse(out.Stderr.String())
|
||||
} else {
|
||||
status = ghAuthStatusResponse(out.Stdout.String())
|
||||
}
|
||||
log.DebugContext(ctx, "gh auth status", "gh_auth_status", status)
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
// getGhVersion retrieves the version of 'gh'.
|
||||
func getGhVersion(ctx context.Context) (string, error) {
|
||||
out, err := util.RunCmd(ctx, "gh", "--version")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Split(out.Stdout.String(), "\n")[0], nil
|
||||
}
|
||||
|
||||
// guideToInstallGh guides the user towards installing the GitHub CLI.
|
||||
func guideToInstallGh(ctx context.Context) error {
|
||||
log := logger.FromContext(ctx)
|
||||
log.WarnContext(ctx, "The GitHub CLI is required to set up Holos. To install it, follow the instructions at: https://github.com/cli/cli#installation")
|
||||
return wrapper.Wrap(fmt.Errorf("GitHub CLI is not installed"))
|
||||
}
|
||||
|
||||
// authenticateGh runs 'gh auth login' to authenticate the GitHub CLI.
|
||||
func authenticateGh(ctx context.Context, cfg *config) error {
|
||||
log := logger.FromContext(ctx)
|
||||
log.InfoContext(ctx, "Authenticating GitHub CLI with 'gh auth login --hostname="+*cfg.githubInstance+"'. Please follow the prompts.")
|
||||
|
||||
err := util.RunInteractiveCmd(ctx, "gh", "auth", "login", "--hostname="+*cfg.githubInstance)
|
||||
if err != nil {
|
||||
log.ErrorContext(ctx, "Failed to authenticate GitHub CLI")
|
||||
return wrapper.Wrap(fmt.Errorf("failed to authenticate GitHub CLI: %w", err))
|
||||
}
|
||||
|
||||
log.InfoContext(ctx, "GitHub CLI has been authenticated.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ghIsAuthenticated checks if the GitHub CLI is authenticated and logged in to githubInstance.
|
||||
func ghIsAuthenticated(status ghAuthStatusResponse, cfg *config) bool {
|
||||
return strings.Contains(string(status), "Logged in to "+*cfg.githubInstance)
|
||||
}
|
||||
|
||||
// ghTokenAllowsRepoCreation validates that the GitHub CLI is authenticated
|
||||
// with a token that allows repository creation. This is a naive implementation
|
||||
// that just checks the output of 'gh auth status' for the presence of the
|
||||
// 'repo' scope. Note that the 'repo' scope is sufficient to create a secret in
|
||||
// a repository, so this check also covers that.
|
||||
// Example token scope line: "- Token scopes: 'admin:public_key', 'gist', 'read:org', 'repo'"
|
||||
func ghTokenAllowsRepoCreation(status ghAuthStatusResponse) bool {
|
||||
return strings.Contains(string(status), "'repo'")
|
||||
}
|
||||
36
pkg/cli/preflight/gh_test.go
Normal file
36
pkg/cli/preflight/gh_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package preflight
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGhTokenAllowsRepoCreation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
status ghAuthStatusResponse
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "token has necessary scopes",
|
||||
status: "- Token: gho_************************************\n- Token scopes: 'gist', 'read:org', 'repo'",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "token has necessary scopes",
|
||||
status: " - Token scopes: 'gist', 'read:org', 'repo'",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "token does not have necessary scopes",
|
||||
status: "- Token: gho_************************************\n- Token scopes: 'gist', 'read:org'",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := ghTokenAllowsRepoCreation(tc.status)
|
||||
if result != tc.expected {
|
||||
t.Errorf("expected %v, got %v", tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
pkg/cli/preflight/preflight.go
Normal file
55
pkg/cli/preflight/preflight.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package preflight
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/holos-run/holos/pkg/cli/command"
|
||||
"github.com/holos-run/holos/pkg/holos"
|
||||
"github.com/holos-run/holos/pkg/logger"
|
||||
)
|
||||
|
||||
// Config holds configuration parameters for preflight checks.
|
||||
type config struct {
|
||||
githubInstance *string
|
||||
}
|
||||
|
||||
// Build the shared configuration and flagset for the preflight command.
|
||||
func newConfig() (*config, *flag.FlagSet) {
|
||||
cfg := &config{}
|
||||
flagSet := flag.NewFlagSet("", flag.ContinueOnError)
|
||||
cfg.githubInstance = flagSet.String("github-instance", "github.com", "Address of the GitHub instance you want to use")
|
||||
|
||||
return cfg, flagSet
|
||||
}
|
||||
|
||||
// New returns the preflight command for the root command.
|
||||
func New(hc *holos.Config) *cobra.Command {
|
||||
cfg, flagSet := newConfig()
|
||||
|
||||
cmd := command.New("preflight")
|
||||
cmd.Short = "Run preflight checks to ensure you're ready to use Holos"
|
||||
cmd.Flags().AddGoFlagSet(flagSet)
|
||||
cmd.RunE = makePreflightRunFunc(hc, cfg)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// makePreflightRunFunc returns the internal implementation of the preflight cli command.
|
||||
func makePreflightRunFunc(_ *holos.Config, cfg *config) command.RunFunc {
|
||||
return func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
log := logger.FromContext(ctx)
|
||||
log.Info("Starting preflight checks")
|
||||
|
||||
// GitHub checks
|
||||
if err := RunGhChecks(ctx, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
// Other checks can be added here
|
||||
|
||||
log.Info("Preflight checks complete. Ready to use Holos 🚀")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/holos-run/holos/pkg/cli/build"
|
||||
"github.com/holos-run/holos/pkg/cli/create"
|
||||
"github.com/holos-run/holos/pkg/cli/get"
|
||||
"github.com/holos-run/holos/pkg/cli/kv"
|
||||
"github.com/holos-run/holos/pkg/cli/preflight"
|
||||
"github.com/holos-run/holos/pkg/cli/render"
|
||||
"github.com/holos-run/holos/pkg/cli/txtar"
|
||||
"github.com/holos-run/holos/pkg/holos"
|
||||
"github.com/holos-run/holos/pkg/logger"
|
||||
"github.com/holos-run/holos/pkg/version"
|
||||
"github.com/spf13/cobra"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// New returns a new root *cobra.Command for command line execution.
|
||||
@@ -51,6 +54,7 @@ func New(cfg *holos.Config) *cobra.Command {
|
||||
rootCmd.AddCommand(render.New(cfg))
|
||||
rootCmd.AddCommand(get.New(cfg))
|
||||
rootCmd.AddCommand(create.New(cfg))
|
||||
rootCmd.AddCommand(preflight.New(cfg))
|
||||
|
||||
// Maybe not needed?
|
||||
rootCmd.AddCommand(txtar.New(cfg))
|
||||
|
||||
7
pkg/cli/secret/testdata/issue34_print_data.txt
vendored
Normal file
7
pkg/cli/secret/testdata/issue34_print_data.txt
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Print the data key by default
|
||||
holos get secret zitadel-admin
|
||||
stdout '^{$'
|
||||
stdout '^ "url": "https://login.example.com"'
|
||||
stdout '^ "username": "zitadel-admin@zitadel.login.example.com"'
|
||||
stdout '^ "password": "Password1!"'
|
||||
stdout '^}$'
|
||||
@@ -4,23 +4,22 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"cuelang.org/go/cue/build"
|
||||
"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"
|
||||
"strings"
|
||||
|
||||
"cuelang.org/go/cue/build"
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
"cuelang.org/go/cue/load"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -214,13 +213,13 @@ func (r *Result) kustomize(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Run kustomize.
|
||||
kOut, err := runCmd(ctx, "kubectl", "kustomize", tempDir)
|
||||
kOut, err := util.RunCmd(ctx, "kubectl", "kustomize", tempDir)
|
||||
if err != nil {
|
||||
log.ErrorContext(ctx, kOut.stderr.String())
|
||||
log.ErrorContext(ctx, kOut.Stderr.String())
|
||||
return wrapper.Wrap(err)
|
||||
}
|
||||
// Replace the accumulated output
|
||||
r.accumulatedOutput = kOut.stdout.String()
|
||||
r.accumulatedOutput = kOut.Stdout.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -379,25 +378,6 @@ func (b *Builder) findCueMod() (dir holos.PathCueMod, err error) {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
type runResult struct {
|
||||
stdout *bytes.Buffer
|
||||
stderr *bytes.Buffer
|
||||
}
|
||||
|
||||
func runCmd(ctx context.Context, name string, args ...string) (result runResult, err error) {
|
||||
result = runResult{
|
||||
stdout: new(bytes.Buffer),
|
||||
stderr: new(bytes.Buffer),
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Stdout = result.stdout
|
||||
cmd.Stderr = result.stderr
|
||||
log := logger.FromContext(ctx)
|
||||
log.DebugContext(ctx, "run: "+name, "name", name, "args", args)
|
||||
err = cmd.Run()
|
||||
return
|
||||
}
|
||||
|
||||
// runHelm provides the values produced by CUE to helm template and returns
|
||||
// the rendered kubernetes api objects in the result.
|
||||
func runHelm(ctx context.Context, hc *HelmChart, r *Result, path holos.PathComponent) error {
|
||||
@@ -411,15 +391,15 @@ func runHelm(ctx context.Context, hc *HelmChart, r *Result, path holos.PathCompo
|
||||
if isNotExist(cachedChartPath) {
|
||||
// Add repositories
|
||||
repo := hc.Chart.Repository
|
||||
out, err := runCmd(ctx, "helm", "repo", "add", repo.Name, repo.URL)
|
||||
out, err := util.RunCmd(ctx, "helm", "repo", "add", repo.Name, repo.URL)
|
||||
if err != nil {
|
||||
log.ErrorContext(ctx, "could not run helm", "stderr", out.stderr.String(), "stdout", out.stdout.String())
|
||||
log.ErrorContext(ctx, "could not run helm", "stderr", out.Stderr.String(), "stdout", out.Stdout.String())
|
||||
return wrapper.Wrap(fmt.Errorf("could not run helm repo add: %w", err))
|
||||
}
|
||||
// Update repository
|
||||
out, err = runCmd(ctx, "helm", "repo", "update", repo.Name)
|
||||
out, err = util.RunCmd(ctx, "helm", "repo", "update", repo.Name)
|
||||
if err != nil {
|
||||
log.ErrorContext(ctx, "could not run helm", "stderr", out.stderr.String(), "stdout", out.stdout.String())
|
||||
log.ErrorContext(ctx, "could not run helm", "stderr", out.Stderr.String(), "stdout", out.Stdout.String())
|
||||
return wrapper.Wrap(fmt.Errorf("could not run helm repo update: %w", err))
|
||||
}
|
||||
// Cache the chart
|
||||
@@ -443,9 +423,9 @@ func runHelm(ctx context.Context, hc *HelmChart, r *Result, path holos.PathCompo
|
||||
|
||||
// Run charts
|
||||
chart := hc.Chart
|
||||
helmOut, err := runCmd(ctx, "helm", "template", "--values", valuesPath, "--namespace", hc.Namespace, "--kubeconfig", "/dev/null", "--version", chart.Version, chart.Name, cachedChartPath)
|
||||
helmOut, err := util.RunCmd(ctx, "helm", "template", "--values", valuesPath, "--namespace", hc.Namespace, "--kubeconfig", "/dev/null", "--version", chart.Version, chart.Name, cachedChartPath)
|
||||
if err != nil {
|
||||
stderr := helmOut.stderr.String()
|
||||
stderr := helmOut.Stderr.String()
|
||||
lines := strings.Split(stderr, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Error:") {
|
||||
@@ -455,7 +435,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.accumulatedOutput = helmOut.stdout.String()
|
||||
r.accumulatedOutput = helmOut.Stdout.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -486,11 +466,11 @@ func cacheChart(ctx context.Context, path holos.PathComponent, chartDir string,
|
||||
defer remove(ctx, cacheTemp)
|
||||
|
||||
chartName := fmt.Sprintf("%s/%s", chart.Repository.Name, chart.Name)
|
||||
helmOut, err := runCmd(ctx, "helm", "pull", "--destination", cacheTemp, "--untar=true", "--version", chart.Version, chartName)
|
||||
helmOut, err := util.RunCmd(ctx, "helm", "pull", "--destination", cacheTemp, "--untar=true", "--version", chart.Version, chartName)
|
||||
if err != nil {
|
||||
return wrapper.Wrap(fmt.Errorf("could not run helm pull: %w", err))
|
||||
}
|
||||
log.Debug("helm pull", "stdout", helmOut.stdout, "stderr", helmOut.stderr)
|
||||
log.Debug("helm pull", "stdout", helmOut.Stdout, "stderr", helmOut.Stderr)
|
||||
|
||||
cachePath := filepath.Join(string(path), chartDir)
|
||||
if err := os.Rename(cacheTemp, cachePath); err != nil {
|
||||
|
||||
56
pkg/util/run.go
Normal file
56
pkg/util/run.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/holos-run/holos/pkg/logger"
|
||||
)
|
||||
|
||||
// runResult holds the stdout and stderr of a command.
|
||||
type RunResult struct {
|
||||
Stdout *bytes.Buffer
|
||||
Stderr *bytes.Buffer
|
||||
}
|
||||
|
||||
// RunCmd runs a command within a context, captures its output, provides debug
|
||||
// logging, and returns the result.
|
||||
// Example:
|
||||
//
|
||||
// result, err := RunCmd(ctx, "echo", "hello")
|
||||
// if err != nil {
|
||||
// return wrapper.Wrap(err)
|
||||
// }
|
||||
// fmt.Println(result.Stdout.String())
|
||||
//
|
||||
// Output:
|
||||
//
|
||||
// "hello\n"
|
||||
func RunCmd(ctx context.Context, name string, args ...string) (result RunResult, err error) {
|
||||
result = RunResult{
|
||||
Stdout: new(bytes.Buffer),
|
||||
Stderr: new(bytes.Buffer),
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Stdout = result.Stdout
|
||||
cmd.Stderr = result.Stderr
|
||||
log := logger.FromContext(ctx)
|
||||
log.DebugContext(ctx, "running: "+name, "name", name, "args", args)
|
||||
err = cmd.Run()
|
||||
return result, err
|
||||
}
|
||||
|
||||
// RunInteractiveCmd runs a command within a context but allows the command to
|
||||
// accept stdin interactively from the user. The caller is expected to handle
|
||||
// errors.
|
||||
func RunInteractiveCmd(ctx context.Context, name string, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
log := logger.FromContext(ctx)
|
||||
log.DebugContext(ctx, "running: "+name, "name", name, "args", args)
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
51
|
||||
53
|
||||
|
||||
Reference in New Issue
Block a user