From 35026df787ebd8cae889fab8976d4fda4f8376ce Mon Sep 17 00:00:00 2001 From: Norbert Gruszka Date: Wed, 6 May 2026 15:48:43 +0200 Subject: [PATCH 1/3] Auto Initialize OpenBao Service --- apis/vshn/v1/vshn_openbao.go | 26 +++ apis/vshn/v1/zz_generated.deepcopy.go | 32 +++ crds/vshn.appcat.vshn.io_vshnopenbaos.yaml | 28 +++ crds/vshn.appcat.vshn.io_xvshnopenbaos.yaml | 30 +++ .../functions/vshnopenbao/initialize.go | 221 ++++++++++++++++++ .../functions/vshnopenbao/initialize_test.go | 92 ++++++++ .../functions/vshnopenbao/register.go | 4 + .../vshnopenbao/scripts/init_cluster.sh | 44 ++++ .../initialize/01_first_reconcile.yaml | 35 +++ .../initialize/02_job_completed.yaml | 61 +++++ .../initialize/03_already_initialized.yaml | 61 +++++ 11 files changed, 634 insertions(+) create mode 100644 pkg/comp-functions/functions/vshnopenbao/initialize.go create mode 100644 pkg/comp-functions/functions/vshnopenbao/initialize_test.go create mode 100644 pkg/comp-functions/functions/vshnopenbao/scripts/init_cluster.sh create mode 100644 test/functions/vshnopenbao/initialize/01_first_reconcile.yaml create mode 100644 test/functions/vshnopenbao/initialize/02_job_completed.yaml create mode 100644 test/functions/vshnopenbao/initialize/03_already_initialized.yaml diff --git a/apis/vshn/v1/vshn_openbao.go b/apis/vshn/v1/vshn_openbao.go index f2e399f794..50925346f7 100644 --- a/apis/vshn/v1/vshn_openbao.go +++ b/apis/vshn/v1/vshn_openbao.go @@ -50,8 +50,30 @@ type VSHNOpenBaoSpec struct { WriteConnectionSecretToRef LocalObjectReference `json:"writeConnectionSecretToRef,omitempty"` } +type VSHNOpenBaoSettingsInit struct { + // +kubebuilder:default="true" + RunInitJob bool `json:"runInitJob"` + + // SecretShares is the number of key shares to split the generated master key into. + // +kubebuilder:default=5 + // +kubebuilder:validation:Minimum=1 + SecretShares int `json:"secretShares,omitempty"` + + // SecretThreshold is the number of key shares required to reconstruct the master key. + // +kubebuilder:default=3 + // +kubebuilder:validation:Minimum=1 + SecretThreshold int `json:"secretThreshold,omitempty"` +} + +type VSHNOpenBaoSettings struct { + Init VSHNOpenBaoSettingsInit `json:"init,omitempty"` +} + // VSHNOpenBaoParameters are the configurable fields of a VSHNOpenBao. type VSHNOpenBaoParameters struct { + // OpenBao contains service settings + OpenBao VSHNOpenBaoSettings `json:"openBao,omitempty"` + // Service contains OpenBao DBaaS specific properties Service VSHNOpenBaoServiceSpec `json:"service,omitempty"` @@ -145,6 +167,10 @@ type VSHNOpenBaoStatus struct { // Schedules keeps track of random generated schedules, is overwriten by // schedules set in the service's spec. Schedules VSHNScheduleStatus `json:"schedules,omitempty"` + // InitializationComplete is set to true once the init job has run successfully + // and the root token secret is populated. Prevents the init job from being + // re-created on every reconcile after the TTL expires. + InitializationComplete bool `json:"initializationComplete,omitempty"` } func (v *VSHNOpenBao) GetClaimNamespace() string { diff --git a/apis/vshn/v1/zz_generated.deepcopy.go b/apis/vshn/v1/zz_generated.deepcopy.go index 3d1fc994ae..e0fee03713 100644 --- a/apis/vshn/v1/zz_generated.deepcopy.go +++ b/apis/vshn/v1/zz_generated.deepcopy.go @@ -1785,6 +1785,7 @@ func (in *VSHNOpenBaoList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VSHNOpenBaoParameters) DeepCopyInto(out *VSHNOpenBaoParameters) { *out = *in + out.OpenBao = in.OpenBao out.Service = in.Service out.Size = in.Size in.Scheduling.DeepCopyInto(&out.Scheduling) @@ -1821,6 +1822,37 @@ func (in *VSHNOpenBaoServiceSpec) DeepCopy() *VSHNOpenBaoServiceSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VSHNOpenBaoSettings) DeepCopyInto(out *VSHNOpenBaoSettings) { + *out = *in + out.Init = in.Init +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNOpenBaoSettings. +func (in *VSHNOpenBaoSettings) DeepCopy() *VSHNOpenBaoSettings { + if in == nil { + return nil + } + out := new(VSHNOpenBaoSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VSHNOpenBaoSettingsInit) DeepCopyInto(out *VSHNOpenBaoSettingsInit) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSHNOpenBaoSettingsInit. +func (in *VSHNOpenBaoSettingsInit) DeepCopy() *VSHNOpenBaoSettingsInit { + if in == nil { + return nil + } + out := new(VSHNOpenBaoSettingsInit) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VSHNOpenBaoSizeSpec) DeepCopyInto(out *VSHNOpenBaoSizeSpec) { *out = *in diff --git a/crds/vshn.appcat.vshn.io_vshnopenbaos.yaml b/crds/vshn.appcat.vshn.io_vshnopenbaos.yaml index 9168dee26b..a7599f1da4 100644 --- a/crds/vshn.appcat.vshn.io_vshnopenbaos.yaml +++ b/crds/vshn.appcat.vshn.io_vshnopenbaos.yaml @@ -4631,6 +4631,28 @@ spec: description: Email necessary to send alerts via email type: string type: object + openBao: + description: OpenBao contains service settings + properties: + init: + properties: + runInitJob: + default: "true" + type: boolean + secretShares: + default: 5 + description: SecretShares is the number of key shares to split the generated master key into. + minimum: 1 + type: integer + secretThreshold: + default: 3 + description: SecretThreshold is the number of key shares required to reconstruct the master key. + minimum: 1 + type: integer + required: + - runInitJob + type: object + type: object restore: description: Restore contains settings to control the restore of an instance. properties: @@ -4820,6 +4842,12 @@ spec: type: string type: object type: array + initializationComplete: + description: |- + InitializationComplete is set to true once the init job has run successfully + and the root token secret is populated. Prevents the init job from being + re-created on every reconcile after the TTL expires. + type: boolean instanceNamespace: description: InstanceNamespace contains the name of the namespace where the instance resides type: string diff --git a/crds/vshn.appcat.vshn.io_xvshnopenbaos.yaml b/crds/vshn.appcat.vshn.io_xvshnopenbaos.yaml index bd5e26e49a..7aa8316d44 100644 --- a/crds/vshn.appcat.vshn.io_xvshnopenbaos.yaml +++ b/crds/vshn.appcat.vshn.io_xvshnopenbaos.yaml @@ -5352,6 +5352,30 @@ spec: description: Email necessary to send alerts via email type: string type: object + openBao: + description: OpenBao contains service settings + properties: + init: + properties: + runInitJob: + default: "true" + type: boolean + secretShares: + default: 5 + description: SecretShares is the number of key shares + to split the generated master key into. + minimum: 1 + type: integer + secretThreshold: + default: 3 + description: SecretThreshold is the number of key shares + required to reconstruct the master key. + minimum: 1 + type: integer + required: + - runInitJob + type: object + type: object restore: description: Restore contains settings to control the restore of an instance. @@ -5734,6 +5758,12 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + initializationComplete: + description: |- + InitializationComplete is set to true once the init job has run successfully + and the root token secret is populated. Prevents the init job from being + re-created on every reconcile after the TTL expires. + type: boolean instanceNamespace: description: InstanceNamespace contains the name of the namespace where the instance resides diff --git a/pkg/comp-functions/functions/vshnopenbao/initialize.go b/pkg/comp-functions/functions/vshnopenbao/initialize.go new file mode 100644 index 0000000000..87d9b07064 --- /dev/null +++ b/pkg/comp-functions/functions/vshnopenbao/initialize.go @@ -0,0 +1,221 @@ +package vshnopenbao + +import ( + "context" + _ "embed" + "fmt" + + xfnproto "github.com/crossplane/function-sdk-go/proto/v1" + xkube "github.com/vshn/appcat/v4/apis/kubernetes/v1alpha2" + vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" + "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common" + "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +//go:embed scripts/init_cluster.sh +var initClusterScript string + +const ( + initSuffix = "openbao-init" + initOutputSecretSuffix = "-init-output" + unsealKeysSecretSuffix = "-unseal-keys" + initJobSuffix = "-init-job" + observerSuffix = "-observer" +) + +func InitializeCluster(ctx context.Context, comp *vshnv1.VSHNOpenBao, svc *runtime.ServiceRuntime) *xfnproto.Result { + err := svc.GetObservedComposite(comp) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot get composite: %w", err)) + } + + if !comp.Spec.Parameters.OpenBao.Init.RunInitJob { + svc.Log.Info("RunInitJob is set to false. Skip initialize process...") + return nil + } + + // Always observe secrets so connection details keep flowing to the user's secret. + err = observeInitConnectionDetails(comp, svc) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot observe init connection details: %w", err)) + } + + if comp.Status.InitializationComplete { + svc.Log.Info("OpenBao already initialized, skipping init job") + return nil + } + + if isInitSecretPopulated(comp, svc) { + svc.Log.Info("Init secret detected, marking initialization as complete") + comp.Status.InitializationComplete = true + if err = svc.SetDesiredCompositeStatus(comp); err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot set initialization status: %w", err)) + } + return nil + } + + err = createSA(ctx, comp, svc) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot create init RBAC: %w", err)) + } + + err = createInitJob(comp, svc) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("cannot create init job: %w", err)) + } + + return nil +} + +func isInitSecretPopulated(comp *vshnv1.VSHNOpenBao, svc *runtime.ServiceRuntime) bool { + secret := &corev1.Secret{} + err := svc.GetObservedKubeObject(secret, comp.GetName()+initOutputSecretSuffix+observerSuffix) + if err != nil { + return false + } + return len(secret.Data["VAULT_TOKEN"]) > 0 +} + +func createSA(ctx context.Context, comp *vshnv1.VSHNOpenBao, svc *runtime.ServiceRuntime) error { + svc.Log.Info("Create RBAC for OpenBao init job") + return common.AddSaWithRole(ctx, svc, []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"create", "get", "update", "patch"}, + }, + }, comp.GetName(), comp.GetInstanceNamespace(), initSuffix, true) +} + +func createInitJob(comp *vshnv1.VSHNOpenBao, svc *runtime.ServiceRuntime) error { + serviceName := comp.GetName() + ns := comp.GetInstanceNamespace() + + secretShares := comp.Spec.Parameters.OpenBao.Init.SecretShares + secretThreshold := comp.Spec.Parameters.OpenBao.Init.SecretThreshold + rootTokenSecretName := serviceName + initOutputSecretSuffix + unsealKeysSecretName := serviceName + unsealKeysSecretSuffix + jobName := serviceName + initJobSuffix + openbaoAddr := fmt.Sprintf("https://%s:8200", serviceName) + + svc.Log.Info("Creating OpenBao init job") + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: jobName, + Namespace: ns, + }, + Spec: batchv1.JobSpec{ + BackoffLimit: ptr.To(int32(100)), + TTLSecondsAfterFinished: ptr.To(int32(3600)), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + ServiceAccountName: "sa-" + initSuffix, + Containers: []corev1.Container{ + { + Name: "init-openbao", + Image: svc.Config.Data["kubectl_image"], + Command: []string{"bash", "-c"}, + Args: []string{initClusterScript}, + Env: []corev1.EnvVar{ + {Name: "VAULT_ADDR", Value: openbaoAddr}, + {Name: "NAMESPACE", Value: ns}, + {Name: "ROOT_TOKEN_SECRET_NAME", Value: rootTokenSecretName}, + {Name: "UNSEAL_KEYS_SECRET_NAME", Value: unsealKeysSecretName}, + {Name: "SECRET_SHARES", Value: fmt.Sprintf("%d", secretShares)}, + {Name: "SECRET_THRESHOLD", Value: fmt.Sprintf("%d", secretThreshold)}, + }, + }, + }, + }, + }, + }, + } + + return svc.SetDesiredKubeObject(job, jobName, runtime.KubeOptionAllowDeletion) +} + +func observeInitConnectionDetails(comp *vshnv1.VSHNOpenBao, svc *runtime.ServiceRuntime) error { + serviceName := comp.GetName() + ns := comp.GetInstanceNamespace() + + rootTokenSecretName := serviceName + initOutputSecretSuffix + unsealKeysSecretName := serviceName + unsealKeysSecretSuffix + + rootTokenSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: rootTokenSecretName, + Namespace: ns, + }, + } + err := svc.SetDesiredKubeObject(rootTokenSecret, rootTokenSecretName+observerSuffix, + runtime.KubeOptionObserve, + runtime.KubeOptionAllowDeletion, + runtime.KubeOptionAddConnectionDetails(svc.GetCrossplaneNamespace(), + xkube.ConnectionDetail{ + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Namespace: ns, + Name: rootTokenSecretName, + FieldPath: "data.VAULT_ADDR", + }, + ToConnectionSecretKey: "VAULT_ADDR", + }, + xkube.ConnectionDetail{ + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Namespace: ns, + Name: rootTokenSecretName, + FieldPath: "data.VAULT_TOKEN", + }, + ToConnectionSecretKey: "VAULT_TOKEN", + }, + ), + ) + if err != nil { + return fmt.Errorf("cannot set root token secret observer: %w", err) + } + + if err = svc.AddObservedConnectionDetails(rootTokenSecretName + observerSuffix); err != nil { + return fmt.Errorf("cannot add root token connection details: %w", err) + } + + unsealKeysSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: unsealKeysSecretName, + Namespace: ns, + }, + } + err = svc.SetDesiredKubeObject(unsealKeysSecret, unsealKeysSecretName+observerSuffix, + runtime.KubeOptionObserve, + runtime.KubeOptionAllowDeletion, + runtime.KubeOptionAddConnectionDetails(svc.GetCrossplaneNamespace(), + xkube.ConnectionDetail{ + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Namespace: ns, + Name: unsealKeysSecretName, + FieldPath: "data.keys", + }, + ToConnectionSecretKey: "keys", + }, + ), + ) + if err != nil { + return fmt.Errorf("cannot set unseal keys secret observer: %w", err) + } + + if err = svc.AddObservedConnectionDetails(unsealKeysSecretName + observerSuffix); err != nil { + return fmt.Errorf("cannot add unseal keys connection details: %w", err) + } + + return nil +} diff --git a/pkg/comp-functions/functions/vshnopenbao/initialize_test.go b/pkg/comp-functions/functions/vshnopenbao/initialize_test.go new file mode 100644 index 0000000000..097c1ba62f --- /dev/null +++ b/pkg/comp-functions/functions/vshnopenbao/initialize_test.go @@ -0,0 +1,92 @@ +package vshnopenbao + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" + "github.com/vshn/appcat/v4/pkg/comp-functions/functions/commontest" + "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" +) + +// TestOperatorInitializeFirstReconcile covers the case where no init secret exists yet. +// The function should create the RBAC and init job. +func TestOperatorInitializeFirstReconcile(t *testing.T) { + svc := commontest.LoadRuntimeFromFile(t, "vshnopenbao/initialize/01_first_reconcile.yaml") + comp := &vshnv1.VSHNOpenBao{} + require.NoError(t, svc.GetObservedComposite(comp)) + + result := InitializeCluster(context.Background(), comp, svc) + assert.Nil(t, result) + + // Secret observers should always be set up. + rootTokenObserver := &corev1.Secret{} + assert.NoError(t, svc.GetDesiredKubeObject(rootTokenObserver, "openbao-test-init-output-observer")) + unsealKeysObserver := &corev1.Secret{} + assert.NoError(t, svc.GetDesiredKubeObject(unsealKeysObserver, "openbao-test-unseal-keys-observer")) + + // SA and init job should be created. + sa := &corev1.ServiceAccount{} + assert.NoError(t, svc.GetDesiredKubeObject(sa, "openbao-test-openbao-init-serviceaccount")) + job := &batchv1.Job{} + assert.NoError(t, svc.GetDesiredKubeObject(job, "openbao-test-init-job")) + assert.Equal(t, "vshn-openbao-openbao-test", job.Namespace) + assert.Equal(t, "bitnami/kubectl:latest", job.Spec.Template.Spec.Containers[0].Image) + + // InitializationComplete must not be set in desired composite yet. + desired := &vshnv1.VSHNOpenBao{} + require.NoError(t, svc.GetDesiredComposite(desired)) + assert.False(t, desired.Status.InitializationComplete) +} + +// TestOperatorInitializeJobCompleted covers the reconcile immediately after the init job +// writes the root token secret. The function should set InitializationComplete=true and +// skip creating the job. +func TestOperatorInitializeJobCompleted(t *testing.T) { + svc := commontest.LoadRuntimeFromFile(t, "vshnopenbao/initialize/02_job_completed.yaml") + comp := &vshnv1.VSHNOpenBao{} + require.NoError(t, svc.GetObservedComposite(comp)) + + result := InitializeCluster(context.Background(), comp, svc) + assert.Nil(t, result) + + // Secret observers should still be set up. + rootTokenObserver := &corev1.Secret{} + assert.NoError(t, svc.GetDesiredKubeObject(rootTokenObserver, "openbao-test-init-output-observer")) + + // Job and SA must not be in the desired state. + job := &batchv1.Job{} + assert.ErrorIs(t, svc.GetDesiredKubeObject(job, "openbao-test-init-job"), runtime.ErrNotFound) + sa := &corev1.ServiceAccount{} + assert.ErrorIs(t, svc.GetDesiredKubeObject(sa, "sa-openbao-init"), runtime.ErrNotFound) + + // InitializationComplete must be persisted to the desired composite. + desired := &vshnv1.VSHNOpenBao{} + require.NoError(t, svc.GetDesiredComposite(desired)) + assert.True(t, desired.Status.InitializationComplete) +} + +// TestOperatorInitializeAlreadyInitialized covers the steady-state reconcile after the +// status flag has been persisted. The function should be a no-op for the job and RBAC. +func TestOperatorInitializeAlreadyInitialized(t *testing.T) { + svc := commontest.LoadRuntimeFromFile(t, "vshnopenbao/initialize/03_already_initialized.yaml") + comp := &vshnv1.VSHNOpenBao{} + require.NoError(t, svc.GetObservedComposite(comp)) + + result := InitializeCluster(context.Background(), comp, svc) + assert.Nil(t, result) + + // Secret observers should still be set up so connection details keep flowing. + rootTokenObserver := &corev1.Secret{} + assert.NoError(t, svc.GetDesiredKubeObject(rootTokenObserver, "openbao-test-init-output-observer")) + + // Job and SA must not be recreated. + job := &batchv1.Job{} + assert.ErrorIs(t, svc.GetDesiredKubeObject(job, "openbao-test-init-job"), runtime.ErrNotFound) + sa := &corev1.ServiceAccount{} + assert.ErrorIs(t, svc.GetDesiredKubeObject(sa, "sa-openbao-init"), runtime.ErrNotFound) +} diff --git a/pkg/comp-functions/functions/vshnopenbao/register.go b/pkg/comp-functions/functions/vshnopenbao/register.go index 6608755abf..f15f6a6ab5 100644 --- a/pkg/comp-functions/functions/vshnopenbao/register.go +++ b/pkg/comp-functions/functions/vshnopenbao/register.go @@ -28,6 +28,10 @@ func init() { Name: "deploy-openbao", Execute: DeployOpenBao, }, + { + Name: "initialize", + Execute: InitializeCluster, + }, }, }) } diff --git a/pkg/comp-functions/functions/vshnopenbao/scripts/init_cluster.sh b/pkg/comp-functions/functions/vshnopenbao/scripts/init_cluster.sh new file mode 100644 index 0000000000..f4c2145255 --- /dev/null +++ b/pkg/comp-functions/functions/vshnopenbao/scripts/init_cluster.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +echo "Waiting for OpenBao to become reachable..." +until curl -sk "${VAULT_ADDR}/v1/sys/health" > /dev/null 2>&1; do + echo " not ready, retrying in 5s..." + sleep 5 +done + +INIT_STATUS=$(curl -sk "${VAULT_ADDR}/v1/sys/init" | jq -r '.initialized') +if [ "${INIT_STATUS}" = "true" ]; then + echo "Already initialized, nothing to do." + exit 0 +fi + +echo "Initializing OpenBao..." +INIT_RESPONSE=$(curl -sk --request POST \ + --data "{\"secret_shares\": ${SECRET_SHARES}, \"secret_threshold\": ${SECRET_THRESHOLD}}" \ + "${VAULT_ADDR}/v1/sys/init") + +ROOT_TOKEN=$(echo "${INIT_RESPONSE}" | jq -r '.root_token') +if [ -z "${ROOT_TOKEN}" ] || [ "${ROOT_TOKEN}" = "null" ]; then + echo "ERROR: no root_token in response" + exit 1 +fi + +KEYS_JSON=$(echo "${INIT_RESPONSE}" | jq 'del(.root_token)') + +# kubectl create secret fails with "already exists" if the secret was already created (e.g. job retries, pod restarts). +# The pipe pattern combines both: `create --dry-run=client -o yaml` generates the manifest locally without hitting +# the API, then `kubectl apply -f` - creates or updates it safely. +# +# The `--save-config` adds the last-applied-configuration annotation so future kubectl apply calls can do a proper +# three-way merge diff. +echo "Storing init output to secret ${ROOT_TOKEN_SECRET_NAME}..." +kubectl -n "${NAMESPACE}" create secret generic "${ROOT_TOKEN_SECRET_NAME}" \ + --from-literal=VAULT_ADDR="${VAULT_ADDR}" \ + --from-literal=VAULT_TOKEN="${ROOT_TOKEN}" \ + --save-config --dry-run=client -o yaml | kubectl apply -f - + +echo "Storing unseal/recovery keys to secret ${UNSEAL_KEYS_SECRET_NAME}..." +kubectl -n "${NAMESPACE}" create secret generic "${UNSEAL_KEYS_SECRET_NAME}" \ + --from-literal=keys="${KEYS_JSON}" \ + --save-config --dry-run=client -o yaml | kubectl apply -f - diff --git a/test/functions/vshnopenbao/initialize/01_first_reconcile.yaml b/test/functions/vshnopenbao/initialize/01_first_reconcile.yaml new file mode 100644 index 0000000000..67f26901f7 --- /dev/null +++ b/test/functions/vshnopenbao/initialize/01_first_reconcile.yaml @@ -0,0 +1,35 @@ +desired: {} +input: + apiVersion: v1 + kind: ConfigMap + metadata: + annotations: {} + labels: + name: xfn-config + name: xfn-config + data: + defaultPlan: standard-2 + controlNamespace: appcat-control + kubectl_image: bitnami/kubectl:latest + plans: '{"standard-2": {"size": {"cpu": "500m", "disk": "16Gi", "enabled": true, + "memory": "2Gi"}}}' +observed: + composite: + resource: + apiVersion: vshn.appcat.vshn.io/v1 + kind: XVSHNOpenBao + metadata: + name: openbao-test + labels: + crossplane.io/claim-name: openbao-test + crossplane.io/claim-namespace: unit-test + spec: + parameters: + openBao: + init: + runInitJob: true + secretShares: 5 + secretThreshold: 3 + status: + instanceNamespace: vshn-openbao-openbao-test + resources: {} diff --git a/test/functions/vshnopenbao/initialize/02_job_completed.yaml b/test/functions/vshnopenbao/initialize/02_job_completed.yaml new file mode 100644 index 0000000000..e6ca8f569e --- /dev/null +++ b/test/functions/vshnopenbao/initialize/02_job_completed.yaml @@ -0,0 +1,61 @@ +desired: {} +input: + apiVersion: v1 + kind: ConfigMap + metadata: + annotations: {} + labels: + name: xfn-config + name: xfn-config + data: + defaultPlan: standard-2 + controlNamespace: appcat-control + kubectl_image: bitnami/kubectl:latest + plans: '{"standard-2": {"size": {"cpu": "500m", "disk": "16Gi", "enabled": true, + "memory": "2Gi"}}}' +observed: + composite: + resource: + apiVersion: vshn.appcat.vshn.io/v1 + kind: XVSHNOpenBao + metadata: + name: openbao-test + labels: + crossplane.io/claim-name: openbao-test + crossplane.io/claim-namespace: unit-test + spec: + parameters: + openBao: + init: + runInitJob: true + secretShares: 5 + secretThreshold: 3 + status: + instanceNamespace: vshn-openbao-openbao-test + initializationComplete: false + resources: + openbao-test-init-output-observer: + resource: + apiVersion: kubernetes.crossplane.io/v1alpha2 + kind: Object + metadata: + name: openbao-test-init-output-observer + spec: + forProvider: + manifest: + apiVersion: v1 + kind: Secret + metadata: + name: openbao-test-init-output + namespace: vshn-openbao-openbao-test + status: + atProvider: + manifest: + apiVersion: v1 + kind: Secret + metadata: + name: openbao-test-init-output + namespace: vshn-openbao-openbao-test + data: + VAULT_TOKEN: dGVzdC1yb290LXRva2Vu + VAULT_ADDR: aHR0cHM6Ly9vcGVuYmFvLXRlc3Q6ODIwMA== diff --git a/test/functions/vshnopenbao/initialize/03_already_initialized.yaml b/test/functions/vshnopenbao/initialize/03_already_initialized.yaml new file mode 100644 index 0000000000..6d4360184e --- /dev/null +++ b/test/functions/vshnopenbao/initialize/03_already_initialized.yaml @@ -0,0 +1,61 @@ +desired: {} +input: + apiVersion: v1 + kind: ConfigMap + metadata: + annotations: {} + labels: + name: xfn-config + name: xfn-config + data: + defaultPlan: standard-2 + controlNamespace: appcat-control + kubectl_image: bitnami/kubectl:latest + plans: '{"standard-2": {"size": {"cpu": "500m", "disk": "16Gi", "enabled": true, + "memory": "2Gi"}}}' +observed: + composite: + resource: + apiVersion: vshn.appcat.vshn.io/v1 + kind: XVSHNOpenBao + metadata: + name: openbao-test + labels: + crossplane.io/claim-name: openbao-test + crossplane.io/claim-namespace: unit-test + spec: + parameters: + openBao: + init: + runInitJob: true + secretShares: 5 + secretThreshold: 3 + status: + instanceNamespace: vshn-openbao-openbao-test + initializationComplete: true + resources: + openbao-test-init-output-observer: + resource: + apiVersion: kubernetes.crossplane.io/v1alpha2 + kind: Object + metadata: + name: openbao-test-init-output-observer + spec: + forProvider: + manifest: + apiVersion: v1 + kind: Secret + metadata: + name: openbao-test-init-output + namespace: vshn-openbao-openbao-test + status: + atProvider: + manifest: + apiVersion: v1 + kind: Secret + metadata: + name: openbao-test-init-output + namespace: vshn-openbao-openbao-test + data: + VAULT_TOKEN: dGVzdC1yb290LXRva2Vu + VAULT_ADDR: aHR0cHM6Ly9vcGVuYmFvLXRlc3Q6ODIwMA== From 000cf8f0c01eef00e2a54ab218cf71f6430b1a20 Mon Sep 17 00:00:00 2001 From: Norbert Gruszka Date: Thu, 7 May 2026 14:02:49 +0200 Subject: [PATCH 2/3] Regenerate VSHNOpenBao CRD --- crds/vshn.appcat.vshn.io_vshnopenbaos.yaml | 2 +- crds/vshn.appcat.vshn.io_xvshnopenbaos.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crds/vshn.appcat.vshn.io_vshnopenbaos.yaml b/crds/vshn.appcat.vshn.io_vshnopenbaos.yaml index a7599f1da4..9f01d601bc 100644 --- a/crds/vshn.appcat.vshn.io_vshnopenbaos.yaml +++ b/crds/vshn.appcat.vshn.io_vshnopenbaos.yaml @@ -4637,7 +4637,7 @@ spec: init: properties: runInitJob: - default: "true" + default: true type: boolean secretShares: default: 5 diff --git a/crds/vshn.appcat.vshn.io_xvshnopenbaos.yaml b/crds/vshn.appcat.vshn.io_xvshnopenbaos.yaml index 7aa8316d44..c22fccae69 100644 --- a/crds/vshn.appcat.vshn.io_xvshnopenbaos.yaml +++ b/crds/vshn.appcat.vshn.io_xvshnopenbaos.yaml @@ -5358,7 +5358,7 @@ spec: init: properties: runInitJob: - default: "true" + default: true type: boolean secretShares: default: 5 From 12b077b34d11934d8e4bc03d65895116c6cd9b02 Mon Sep 17 00:00:00 2001 From: Norbert Gruszka Date: Thu, 7 May 2026 14:10:06 +0200 Subject: [PATCH 3/3] Regenerate VSHNOpenBao CRD --- apis/vshn/v1/vshn_openbao.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/vshn/v1/vshn_openbao.go b/apis/vshn/v1/vshn_openbao.go index 50925346f7..86fe0deaa0 100644 --- a/apis/vshn/v1/vshn_openbao.go +++ b/apis/vshn/v1/vshn_openbao.go @@ -51,7 +51,7 @@ type VSHNOpenBaoSpec struct { } type VSHNOpenBaoSettingsInit struct { - // +kubebuilder:default="true" + // +kubebuilder:default=true RunInitJob bool `json:"runInitJob"` // SecretShares is the number of key shares to split the generated master key into.