Compare commits

..

3 Commits

Author SHA1 Message Date
Jeff McCune
4c5429b64a (#22) Ceph CSI for Metal clusters
This patch adds the ceph-csi-rbd helm chart component to the metal
cluster type.  The purpose is to enable PersistentVolumeClaims on ois
metal clusters.

Cloud clusters like GKE and EKS are expected to skip rendering the metal
type.

Helm values are handled with CUE.  The ceph secret is managed as an
ExternalSecret resource, appended to the rendered output by cue and the
holos cli.

Use:

    ❯ holos render --cluster-name=k2 ~/workspace/holos-run/holos/docs/examples/platforms/reference/clusters/metal/...
    2:45PM INF render.go:40 rendered prod-metal-ceph version=0.47.0 status=ok action=rendered name=prod-metal-ceph
2024-02-28 14:46:03 -08:00
Jeff McCune
ac5bff4b32 (#20) Error if secret is not found
Without this patch scripts incorrectly proceeded without detecting a
secret was not fetched.

    holos get secret notfound

    8:34AM ERR could not execute version=0.46.3 err="not found: notfound" loc=get.go:66
2024-02-28 08:33:55 -08:00
Jeff McCune
6090ab224e (#14) Validate secrets fetched from provisioner cluster
This patch validates secrets are synced from the provisioner cluster to
a workload cluster.  This verifies the eso-creds-refresher job, external
secrets operator, etc...

Refer to
0ae58858f5
for the corresponding commit on the k2 cluster.
2024-02-27 15:55:17 -08:00
19 changed files with 285 additions and 52 deletions

View File

@@ -1,16 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="render platform" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="holos" />
<working_directory value="$PROJECT_DIR$/../holos-infra" />
<parameters value="render --cluster-name=k2 $PROJECT_DIR$/docs/examples/platforms/reference/clusters/workload/..." />
<envs>
<env name="DEBUG" value="1" />
</envs>
<kind value="DIRECTORY" />
<package value="github.com/holos-run/holos" />
<directory value="$PROJECT_DIR$/cmd/holos" />
<filePath value="$PROJECT_DIR$" />
<output_directory value="$PROJECT_DIR$/bin" />
<method v="2" />
</configuration>
</component>

View File

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

View File

@@ -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));')"
}
}

View File

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

View File

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

View File

@@ -13,10 +13,7 @@ package holos
objects: [
#SecretStore,
#ExternalSecret & {
_name: "validate"
spec: data: [{remoteRef: key: _name}]
},
#ExternalSecret & { _name: "validate" },
]
{} & #KubernetesObjects

View File

@@ -1,5 +1,7 @@
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"},

View File

@@ -79,10 +79,10 @@ _apiVersion: "holos.run/v1alpha1"
kind: string | *"GitRepository"
name: string | *"flux-system"
}
suspend?: bool
suspend?: bool
targetNamespace?: string
timeout: string | *"3m0s"
wait: bool | *true
timeout: string | *"3m0s"
wait: bool | *true
}
}
@@ -102,6 +102,10 @@ _apiVersion: "holos.run/v1alpha1"
target: {
creationPolicy: string | *"Owner"
}
data: [{
remoteRef: key: _name
secretKey: _name
}]
}
}
@@ -115,11 +119,11 @@ _apiVersion: "holos.run/v1alpha1"
remoteNamespace: #TargetNamespace
auth: token: bearerToken: {
name: string | *"eso-reader"
key: string | *"token"
key: string | *"token"
}
server: {
caBundle: #InputKeys.provisionerCABundle
url: #InputKeys.provisionerURL
url: #InputKeys.provisionerURL
}
}
}
@@ -142,9 +146,9 @@ _apiVersion: "holos.run/v1alpha1"
gcpProjectID: string @tag(gcpProjectID, type=string)
gcpProjectNumber: int @tag(gcpProjectNumber, type=int)
// Same as cluster certificate-authority-data field in ~/.holos/kubeconfig.provisioner
// 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
// Same as the cluster server field in ~/.holos/kubeconfig.provisioner
provisionerURL: string @tag(provisionerURL, type=string)
}
@@ -195,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
@@ -215,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
@@ -228,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.

View File

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

View File

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

View File

@@ -16,14 +16,14 @@ 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

View File

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

View File

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

View 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'

View File

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

View File

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

View File

@@ -1 +1 @@
46
47

View File

@@ -1 +1 @@
1
0