Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@ node_modules/
.cursor
.envrc
mise.toml
/PLAN.md
16 changes: 13 additions & 3 deletions api/v1alpha1/secret_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ type SecretSpec struct {
// +optional
Location string `json:"location"`

// the value should be base64 encoded
// Data contains text secret data.
// +optional
Data map[string]string `json:"data,omitempty"`

// SecretRef is the reference to the kubernetes secret
// BinaryData contains binary secret data. Values must be base64-encoded raw bytes.
// +optional
BinaryData map[string]string `json:"binaryData,omitempty"`

// SecretRef is the reference to the kubernetes secret.
// When SecretRef is set, it will be used to fetch the secret data.
// Data will be ignored.
// Data and BinaryData values set directly on this resource take precedence.
// +optional
SecretRef *KubernetesSecretReference `json:"secretRef,omitempty"`

Expand Down Expand Up @@ -110,6 +114,12 @@ func (r PoolMemberReference) ToNamespacedName() types.NamespacedName {
type KubernetesSecretReference struct {
Namespace string `json:"namespace" protobuf:"bytes,1,opt,name=namespace"`
Name string `json:"name" protobuf:"bytes,2,opt,name=name"`

// BinaryDataKeys are keys from the referenced Kubernetes Secret data that should be sent
// to StreamNative Cloud as binaryData. Other keys keep the existing text data behavior.
// +optional
// +listType=set
BinaryDataKeys []string `json:"binaryDataKeys,omitempty" protobuf:"bytes,3,rep,name=binaryDataKeys"`
}

func (r KubernetesSecretReference) ToNamespacedName() types.NamespacedName {
Expand Down
14 changes: 13 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,16 @@ spec:
type: string
type: object
x-kubernetes-map-type: atomic
binaryData:
additionalProperties:
type: string
description: BinaryData contains binary secret data. Values must be
base64-encoded raw bytes.
type: object
data:
additionalProperties:
type: string
description: the value should be base64 encoded
description: Data contains text secret data.
type: object
instanceName:
description: InstanceName is the name of the instance this secret
Expand All @@ -105,10 +111,18 @@ spec:
type: string
secretRef:
description: |-
SecretRef is the reference to the kubernetes secret
SecretRef is the reference to the kubernetes secret.
When SecretRef is set, it will be used to fetch the secret data.
Data will be ignored.
Data and BinaryData values set directly on this resource take precedence.
properties:
binaryDataKeys:
description: |-
BinaryDataKeys are keys from the referenced Kubernetes Secret data that should be sent
to StreamNative Cloud as binaryData. Other keys keep the existing text data behavior.
items:
type: string
type: array
x-kubernetes-list-type: set
name:
type: string
namespace:
Expand Down
20 changes: 17 additions & 3 deletions config/crd/bases/resource.streamnative.io_secrets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,16 @@ spec:
type: string
type: object
x-kubernetes-map-type: atomic
binaryData:
additionalProperties:
type: string
description: BinaryData contains binary secret data. Values must be
base64-encoded raw bytes.
type: object
data:
additionalProperties:
type: string
description: the value should be base64 encoded
description: Data contains text secret data.
type: object
instanceName:
description: InstanceName is the name of the instance this secret
Expand All @@ -105,10 +111,18 @@ spec:
type: string
secretRef:
description: |-
SecretRef is the reference to the kubernetes secret
SecretRef is the reference to the kubernetes secret.
When SecretRef is set, it will be used to fetch the secret data.
Data will be ignored.
Data and BinaryData values set directly on this resource take precedence.
properties:
binaryDataKeys:
description: |-
BinaryDataKeys are keys from the referenced Kubernetes Secret data that should be sent
to StreamNative Cloud as binaryData. Other keys keep the existing text data behavior.
items:
type: string
type: array
x-kubernetes-list-type: set
name:
type: string
namespace:
Expand Down
2 changes: 2 additions & 0 deletions config/samples/resource_v1alpha1_secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ spec:
type: Opaque
data:
key: value
binaryData:
keystore.p12: AAEC/w==
location: "useast1"
instanceName: "test-instance"
78 changes: 58 additions & 20 deletions controllers/secret_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package controllers

import (
"context"
"encoding/base64"
"fmt"
"time"

Expand Down Expand Up @@ -49,29 +50,52 @@ type SecretReconciler struct {
//+kubebuilder:rbac:groups=resource.streamnative.io,resources=streamnativecloudconnections,verbs=get;list;watch
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch

// getSecretData obtains the Secret data either from direct Data field or from SecretRef
func (r *SecretReconciler) getSecretData(ctx context.Context, secretCR *resourcev1alpha1.Secret) (map[string]string, *corev1.SecretType, error) {
// If direct data is provided, use it
if len(secretCR.Spec.Data) > 0 {
return secretCR.Spec.Data, secretCR.Spec.Type, nil
// resolveSecretRefData obtains Secret data from a referenced Kubernetes Secret.
func (r *SecretReconciler) resolveSecretRefData(
ctx context.Context,
secretCR *resourcev1alpha1.Secret,
) (map[string]string, map[string]string, *corev1.SecretType, error) {
if secretCR.Spec.SecretRef == nil {
return nil, nil, nil, fmt.Errorf("SecretRef is not specified in the Secret spec")
}

// If SecretRef is provided, fetch from the referenced Kubernetes Secret
if secretCR.Spec.SecretRef != nil {
nsName := secretCR.Spec.SecretRef.ToNamespacedName()
k8sSecret := &corev1.Secret{}
if err := r.Get(ctx, nsName, k8sSecret); err != nil {
return nil, nil, fmt.Errorf("failed to get referenced Secret %s/%s: %w", nsName.Namespace, nsName.Name, err)
nsName := secretCR.Spec.SecretRef.ToNamespacedName()
k8sSecret := &corev1.Secret{}
if err := r.Get(ctx, nsName, k8sSecret); err != nil {
return nil, nil, nil, fmt.Errorf("failed to get referenced Secret %s/%s: %w", nsName.Namespace, nsName.Name, err)
}

binaryDataKeys := make(map[string]struct{}, len(secretCR.Spec.SecretRef.BinaryDataKeys))
for _, key := range secretCR.Spec.SecretRef.BinaryDataKeys {
binaryDataKeys[key] = struct{}{}
}

stringData := make(map[string]string)
binaryData := make(map[string]string)
for key, value := range k8sSecret.Data {
if _, ok := binaryDataKeys[key]; ok {
binaryData[key] = base64.StdEncoding.EncodeToString(value)
continue
}
stringData[key] = string(value)
}

stringData := make(map[string]string)
for k, v := range k8sSecret.Data {
stringData[k] = string(v)
for key := range binaryDataKeys {
if _, ok := k8sSecret.Data[key]; !ok {
return nil, nil, nil, fmt.Errorf("binaryData key %q not found in referenced Secret %s/%s", key, nsName.Namespace, nsName.Name)
}
return stringData, &k8sSecret.Type, nil
}

return nil, nil, fmt.Errorf("neither Data nor SecretRef is specified in the Secret spec")
return stringData, binaryData, &k8sSecret.Type, nil
}

func validateSecretDataKeys(secretCR *resourcev1alpha1.Secret) error {
for key := range secretCR.Spec.Data {
if _, ok := secretCR.Spec.BinaryData[key]; ok {
return fmt.Errorf("secret data key %q is set in both spec.data and spec.binaryData", key)
}
}
return nil
}

// Reconcile handles the reconciliation of Secret objects
Expand Down Expand Up @@ -102,6 +126,11 @@ func (r *SecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, err // No requeue for permanent misconfiguration
}

if err := validateSecretDataKeys(secretCR); err != nil {
r.updateSecretStatus(ctx, secretCR, err, "ValidationFailed", err.Error())
return ctrl.Result{}, err
}

// Get StreamNativeCloudConnection
connection := &resourcev1alpha1.StreamNativeCloudConnection{}
connErr := r.Get(ctx, types.NamespacedName{
Expand Down Expand Up @@ -183,29 +212,37 @@ func (r *SecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{Requeue: true}, nil
}

// Resolve secret data from Spec.Data or Spec.SecretRef
// The secretCR passed to cloud client methods should have its Spec.Data and Spec.Type populated.
// Resolve secret data from Spec.Data, Spec.BinaryData, or Spec.SecretRef.
// The secretCR passed to cloud client methods should have its Spec.Data, Spec.BinaryData, and Spec.Type populated.
currentSpecData := make(map[string]string)
for k, v := range secretCR.Spec.Data {
currentSpecData[k] = v
}
currentSpecBinaryData := make(map[string]string)
for k, v := range secretCR.Spec.BinaryData {
currentSpecBinaryData[k] = v
}
currentSpecType := secretCR.Spec.Type

// If SecretRef is used, we resolve it and update the CR if necessary.
// This ensures that the CR in etcd reflects the data being sent to the cloud API.
if secretCR.Spec.SecretRef != nil {
resolvedData, resolvedType, err := r.getSecretData(ctx, secretCR)
resolvedData, resolvedBinaryData, resolvedType, err := r.resolveSecretRefData(ctx, secretCR)
if err != nil {
r.updateSecretStatus(ctx, secretCR, err, "GetSecretDataFailed", fmt.Sprintf("Failed to get secret data from SecretRef: %v", err))
return ctrl.Result{}, err
}

// Check if an update to the local CR is needed
updateLocalCR := false
if len(secretCR.Spec.Data) == 0 { // Only populate from SecretRef if direct data is not set
if len(secretCR.Spec.Data) == 0 && len(resolvedData) > 0 { // Only populate from SecretRef if direct data is not set
secretCR.Spec.Data = resolvedData
updateLocalCR = true
}
if len(secretCR.Spec.BinaryData) == 0 && len(resolvedBinaryData) > 0 { // Only populate from SecretRef if direct binaryData is not set
secretCR.Spec.BinaryData = resolvedBinaryData
updateLocalCR = true
}
// Only update type if original spec type was nil or empty, and resolvedType is not nil
if (secretCR.Spec.Type == nil || *secretCR.Spec.Type == "") && resolvedType != nil {
secretCR.Spec.Type = resolvedType
Expand All @@ -216,6 +253,7 @@ func (r *SecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
if err := r.Update(ctx, secretCR); err != nil {
// Restore original spec data before status update to avoid inconsistent state reporting
secretCR.Spec.Data = currentSpecData
secretCR.Spec.BinaryData = currentSpecBinaryData
secretCR.Spec.Type = currentSpecType
r.updateSecretStatus(ctx, secretCR, err, "UpdateLocalSecretFailed",
fmt.Sprintf("Failed to update local Secret CR with resolved data: %v", err))
Expand Down
Loading
Loading