diff --git a/apis/v1alpha1/secretproviderclass_types.go b/apis/v1alpha1/secretproviderclass_types.go index 27d88c0eb..d8233eb89 100644 --- a/apis/v1alpha1/secretproviderclass_types.go +++ b/apis/v1alpha1/secretproviderclass_types.go @@ -51,6 +51,16 @@ type SecretObject struct { // annotations of k8s secret object Annotations map[string]string `json:"annotations,omitempty"` Data []*SecretObjectData `json:"data,omitempty"` + // SyncAll can sync all secrets defined in the parameters field of SecretProviderClass + SyncAll bool `json:"syncAll,omitempty"` +} + +// SyncOptions defines the desired state of synced K8s secret objects and replaces SecretObject +type SyncOptions struct { + // syncs all secrets listed in the parameters field of SecretProviderClass + SyncAll bool `json:"syncAll,omitempty"` + // type of K8s secret object + Type string `json:"type,omitempty"` } // SecretProviderClassSpec defines the desired state of SecretProviderClass @@ -60,6 +70,7 @@ type SecretProviderClassSpec struct { // Configuration for specific provider Parameters map[string]string `json:"parameters,omitempty"` SecretObjects []*SecretObject `json:"secretObjects,omitempty"` + SyncOptions SyncOptions `json:"syncOptions,omitempty"` } // ByPodStatus defines the state of SecretProviderClass as seen by diff --git a/charts/secrets-store-csi-driver/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml b/charts/secrets-store-csi-driver/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml index 842883ce2..761828e56 100644 --- a/charts/secrets-store-csi-driver/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml +++ b/charts/secrets-store-csi-driver/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml @@ -45,6 +45,16 @@ spec: provider: description: Configuration for provider name type: string + syncOptions: + description: SyncOptions defines the secret type when syncing all secrets listed in the parameters field + properties: + syncAll: + description: SyncAll as true will sync all secrets defined in the parameters field to K8s + type: boolean + type: + description: type of K8s secret object + type: string + type: object secretObjects: items: description: SecretObject defines the desired state of synced K8s @@ -55,6 +65,9 @@ spec: type: string description: annotations of k8s secret object type: object + syncAll: + description: SyncAll can sync all secrets defined in the parameters field of SecretProviderClass + type: boolean data: items: description: SecretObjectData defines the desired state of diff --git a/config/crd/bases/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml b/config/crd/bases/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml index 842883ce2..761828e56 100644 --- a/config/crd/bases/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml +++ b/config/crd/bases/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml @@ -45,6 +45,16 @@ spec: provider: description: Configuration for provider name type: string + syncOptions: + description: SyncOptions defines the secret type when syncing all secrets listed in the parameters field + properties: + syncAll: + description: SyncAll as true will sync all secrets defined in the parameters field to K8s + type: boolean + type: + description: type of K8s secret object + type: string + type: object secretObjects: items: description: SecretObject defines the desired state of synced K8s @@ -55,6 +65,9 @@ spec: type: string description: annotations of k8s secret object type: object + syncAll: + description: SyncAll can sync all secrets defined in the parameters field of SecretProviderClass + type: boolean data: items: description: SecretObjectData defines the desired state of diff --git a/controllers/secretproviderclasspodstatus_controller.go b/controllers/secretproviderclasspodstatus_controller.go index c546b811c..90b5461f7 100644 --- a/controllers/secretproviderclasspodstatus_controller.go +++ b/controllers/secretproviderclasspodstatus_controller.go @@ -23,34 +23,29 @@ import ( "sync" "time" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/record" + "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/manager" - - "k8s.io/client-go/tools/record" - "k8s.io/klog/v2" - + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/secrets-store-csi-driver/apis/v1alpha1" "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned/scheme" "sigs.k8s.io/secrets-store-csi-driver/pkg/util/fileutil" "sigs.k8s.io/secrets-store-csi-driver/pkg/util/k8sutil" "sigs.k8s.io/secrets-store-csi-driver/pkg/util/secretutil" - - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/secrets-store-csi-driver/pkg/util/spcutil" corev1 "k8s.io/api/core/v1" - v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - apiruntime "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + client "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -65,7 +60,7 @@ const ( type SecretProviderClassPodStatusReconciler struct { client.Client mutex *sync.Mutex - scheme *apiruntime.Scheme + scheme *runtime.Scheme nodeID string reader client.Reader writer client.Writer @@ -121,7 +116,7 @@ func (r *SecretProviderClassPodStatusReconciler) Patcher(ctx context.Context) er } spcPodStatuses := spcPodStatusList.Items - for i := range spcPodStatuses { + for i, spcPodStatus := range spcPodStatuses { spcName := spcPodStatuses[i].Status.SecretProviderClassName spc := &v1alpha1.SecretProviderClass{} namespace := spcPodStatuses[i].Namespace @@ -135,11 +130,12 @@ func (r *SecretProviderClassPodStatusReconciler) Patcher(ctx context.Context) er spcMap[namespace+"/"+spcName] = *spc } // get the pod and check if the pod has a owner reference - pod := &v1.Pod{} + pod := &corev1.Pod{} err = r.reader.Get(ctx, client.ObjectKey{Namespace: namespace, Name: spcPodStatuses[i].Status.PodName}, pod) if err != nil { return fmt.Errorf("failed to fetch pod during patching, err: %+v", err) } + var ownerRefs []metav1.OwnerReference for _, ownerRef := range pod.GetOwnerReferences() { ownerRefs = append(ownerRefs, metav1.OwnerReference{ @@ -149,6 +145,7 @@ func (r *SecretProviderClassPodStatusReconciler) Patcher(ctx context.Context) er Name: ownerRef.Name, }) } + // If a pod has no owner references, then it's a static pod and // doesn't belong to a replicaset. In this case, use the spcps as // owner reference just like we do it today @@ -167,6 +164,19 @@ func (r *SecretProviderClassPodStatusReconciler) Patcher(ctx context.Context) er ownerRefs = append(ownerRefs, ref) } + if spc.Spec.SyncOptions.SyncAll { + files, err := fileutil.GetMountedFiles(spcPodStatus.Status.TargetPath) + if err != nil { + return fmt.Errorf("failed to get mounted files for pod %s/%s: %v", namespace, pod.Name, err) + } else { + if len(spc.Spec.SecretObjects) == 0 { + spc.Spec.SecretObjects = spcutil.BuildSecretObjects(files, secretutil.GetSecretType(strings.TrimSpace(spc.Spec.SyncOptions.Type))) + } else { + spc.Spec.SecretObjects = append(spc.Spec.SecretObjects, spcutil.BuildSecretObjects(files, secretutil.GetSecretType(strings.TrimSpace(spc.Spec.SyncOptions.Type)))...) + } + } + } + for _, secret := range spc.Spec.SecretObjects { key := types.NamespacedName{Name: secret.SecretName, Namespace: namespace} val, exists := secretOwnerMap[key] @@ -238,7 +248,7 @@ func (r *SecretProviderClassPodStatusReconciler) Reconcile(ctx context.Context, // Obtain the full pod metadata. An object reference is needed for sending // events and the UID is helpful for validating the SPCPS TargetPath. - pod := &v1.Pod{} + pod := &corev1.Pod{} if err := r.reader.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: spcPodStatus.Status.PodName}, pod); err != nil { klog.ErrorS(err, "failed to get pod", "pod", klog.ObjectRef{Namespace: req.Namespace, Name: spcPodStatus.Status.PodName}) if apierrors.IsNotFound(err) { @@ -249,7 +259,7 @@ func (r *SecretProviderClassPodStatusReconciler) Reconcile(ctx context.Context, // skip reconcile if the pod is being terminated // or the pod is in succeeded state (for jobs that complete aren't gc yet) // or the pod is in a failed state (all containers get terminated) - if !pod.GetDeletionTimestamp().IsZero() || pod.Status.Phase == v1.PodSucceeded || pod.Status.Phase == v1.PodFailed { + if !pod.GetDeletionTimestamp().IsZero() || pod.Status.Phase == corev1.PodSucceeded || pod.Status.Phase == corev1.PodFailed { klog.V(5).InfoS("pod is being terminated, skipping reconcile", "pod", klog.KObj(pod)) return ctrl.Result{}, nil } @@ -264,7 +274,7 @@ func (r *SecretProviderClassPodStatusReconciler) Reconcile(ctx context.Context, return ctrl.Result{}, err } - if len(spc.Spec.SecretObjects) == 0 { + if len(spc.Spec.SecretObjects) == 0 && !spc.Spec.SyncOptions.SyncAll { klog.InfoS("no secret objects defined for spc, nothing to reconcile", "spc", klog.KObj(spc), "spcps", klog.KObj(spcPodStatus)) return ctrl.Result{}, nil } @@ -289,6 +299,25 @@ func (r *SecretProviderClassPodStatusReconciler) Reconcile(ctx context.Context, klog.ErrorS(err, "failed to get mounted files", "spc", klog.KObj(spc), "pod", klog.KObj(pod), "spcps", klog.KObj(spcPodStatus)) return ctrl.Result{RequeueAfter: 10 * time.Second}, err } + + if spc.Spec.SyncOptions.SyncAll { + if len(spc.Spec.SecretObjects) == 0 { + spc.Spec.SecretObjects = spcutil.BuildSecretObjects(files, secretutil.GetSecretType(strings.TrimSpace(spc.Spec.SyncOptions.Type))) + } else { + spc.Spec.SecretObjects = append(spc.Spec.SecretObjects, spcutil.BuildSecretObjects(files, secretutil.GetSecretType(strings.TrimSpace(spc.Spec.SyncOptions.Type)))...) + } + } + + for _, secretObj := range spc.Spec.SecretObjects { + if secretObj.SyncAll { + if secretutil.GetSecretType(strings.TrimSpace(secretObj.Type)) != corev1.SecretTypeOpaque { + return ctrl.Result{}, fmt.Errorf("secret provider class %s/%s cannot use secretObjects[*].syncAll for non-opaque secrets", spc.Namespace, spc.Name) + } + + spcutil.BuildSecretObjectData(files, secretObj) + } + } + errs := make([]error, 0) for _, secretObj := range spc.Spec.SecretObjects { secretName := strings.TrimSpace(secretObj.SecretName) @@ -428,6 +457,7 @@ func (r *SecretProviderClassPodStatusReconciler) createK8sSecret(ctx context.Con } err := r.writer.Create(ctx, secret) + if err == nil { klog.InfoS("successfully created Kubernetes secret", "secret", klog.ObjectRef{Namespace: namespace, Name: name}) return nil @@ -482,7 +512,7 @@ func (r *SecretProviderClassPodStatusReconciler) patchSecretWithOwnerRef(ctx con // secretExists checks if the secret with name and namespace already exists func (r *SecretProviderClassPodStatusReconciler) secretExists(ctx context.Context, name, namespace string) (bool, error) { - o := &v1.Secret{} + o := &corev1.Secret{} secretKey := types.NamespacedName{ Namespace: namespace, Name: name, diff --git a/deploy/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml b/deploy/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml index 842883ce2..761828e56 100644 --- a/deploy/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml +++ b/deploy/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml @@ -45,6 +45,16 @@ spec: provider: description: Configuration for provider name type: string + syncOptions: + description: SyncOptions defines the secret type when syncing all secrets listed in the parameters field + properties: + syncAll: + description: SyncAll as true will sync all secrets defined in the parameters field to K8s + type: boolean + type: + description: type of K8s secret object + type: string + type: object secretObjects: items: description: SecretObject defines the desired state of synced K8s @@ -55,6 +65,9 @@ spec: type: string description: annotations of k8s secret object type: object + syncAll: + description: SyncAll can sync all secrets defined in the parameters field of SecretProviderClass + type: boolean data: items: description: SecretObjectData defines the desired state of diff --git a/manifest_staging/charts/secrets-store-csi-driver/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml b/manifest_staging/charts/secrets-store-csi-driver/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml index 842883ce2..761828e56 100644 --- a/manifest_staging/charts/secrets-store-csi-driver/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml +++ b/manifest_staging/charts/secrets-store-csi-driver/crds/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml @@ -45,6 +45,16 @@ spec: provider: description: Configuration for provider name type: string + syncOptions: + description: SyncOptions defines the secret type when syncing all secrets listed in the parameters field + properties: + syncAll: + description: SyncAll as true will sync all secrets defined in the parameters field to K8s + type: boolean + type: + description: type of K8s secret object + type: string + type: object secretObjects: items: description: SecretObject defines the desired state of synced K8s @@ -55,6 +65,9 @@ spec: type: string description: annotations of k8s secret object type: object + syncAll: + description: SyncAll can sync all secrets defined in the parameters field of SecretProviderClass + type: boolean data: items: description: SecretObjectData defines the desired state of diff --git a/manifest_staging/deploy/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml b/manifest_staging/deploy/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml index 842883ce2..761828e56 100644 --- a/manifest_staging/deploy/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml +++ b/manifest_staging/deploy/secrets-store.csi.x-k8s.io_secretproviderclasses.yaml @@ -45,6 +45,16 @@ spec: provider: description: Configuration for provider name type: string + syncOptions: + description: SyncOptions defines the secret type when syncing all secrets listed in the parameters field + properties: + syncAll: + description: SyncAll as true will sync all secrets defined in the parameters field to K8s + type: boolean + type: + description: type of K8s secret object + type: string + type: object secretObjects: items: description: SecretObject defines the desired state of synced K8s @@ -55,6 +65,9 @@ spec: type: string description: annotations of k8s secret object type: object + syncAll: + description: SyncAll can sync all secrets defined in the parameters field of SecretProviderClass + type: boolean data: items: description: SecretObjectData defines the desired state of diff --git a/pkg/rotation/reconciler.go b/pkg/rotation/reconciler.go index 312ad9176..890493b7a 100644 --- a/pkg/rotation/reconciler.go +++ b/pkg/rotation/reconciler.go @@ -24,16 +24,10 @@ import ( "strings" "time" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/strategicpatch" - clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" - - v1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/wait" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" @@ -42,18 +36,22 @@ import ( "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/secrets-store-csi-driver/apis/v1alpha1" "sigs.k8s.io/secrets-store-csi-driver/controllers" - "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" - secretsStoreClient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" - internalerrors "sigs.k8s.io/secrets-store-csi-driver/pkg/errors" "sigs.k8s.io/secrets-store-csi-driver/pkg/k8s" - secretsstore "sigs.k8s.io/secrets-store-csi-driver/pkg/secrets-store" "sigs.k8s.io/secrets-store-csi-driver/pkg/util/fileutil" "sigs.k8s.io/secrets-store-csi-driver/pkg/util/k8sutil" "sigs.k8s.io/secrets-store-csi-driver/pkg/util/secretutil" + "sigs.k8s.io/secrets-store-csi-driver/pkg/util/spcutil" "sigs.k8s.io/secrets-store-csi-driver/pkg/version" + + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" + secretsStoreClient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" + internalerrors "sigs.k8s.io/secrets-store-csi-driver/pkg/errors" + secretsstore "sigs.k8s.io/secrets-store-csi-driver/pkg/secrets-store" ) const ( @@ -81,7 +79,7 @@ type Reconciler struct { reporter StatsReporter eventRecorder record.EventRecorder kubeClient kubernetes.Interface - crdClient versioned.Interface + crdClient secretsStoreClient.Interface // cache contains v1.Pod, v1alpha1.SecretProviderClassPodStatus (both filtered on *nodeID), // v1.Secret (filtered on secrets-store.csi.k8s.io/managed=true) cache client.Reader @@ -428,15 +426,35 @@ func (r *Reconciler) reconcile(ctx context.Context, spcps *v1alpha1.SecretProvid } } - if len(spc.Spec.SecretObjects) == 0 { + if len(spc.Spec.SecretObjects) == 0 && !spc.Spec.SyncOptions.SyncAll { klog.InfoS("spc doesn't contain secret objects", "spc", klog.KObj(spc), "pod", klog.KObj(pod), "controller", "rotation") return nil } + files, err := fileutil.GetMountedFiles(spcps.Status.TargetPath) if err != nil { r.generateEvent(pod, v1.EventTypeWarning, k8sSecretRotationFailedReason, fmt.Sprintf("failed to get mounted files, err: %+v", err)) return fmt.Errorf("failed to get mounted files, err: %+v", err) } + + if spc.Spec.SyncOptions.SyncAll { + if len(spc.Spec.SecretObjects) == 0 { + spc.Spec.SecretObjects = spcutil.BuildSecretObjects(files, secretutil.GetSecretType(strings.TrimSpace(spc.Spec.SyncOptions.Type))) + } else { + spc.Spec.SecretObjects = append(spc.Spec.SecretObjects, spcutil.BuildSecretObjects(files, secretutil.GetSecretType(strings.TrimSpace(spc.Spec.SyncOptions.Type)))...) + } + } + + for _, secretObj := range spc.Spec.SecretObjects { + if secretObj.SyncAll { + if secretutil.GetSecretType(strings.TrimSpace(secretObj.Type)) != v1.SecretTypeOpaque { + return fmt.Errorf("secret provider class %s/%s cannot use secretObjects[*].syncAll for non-opaque secrets", spc.Namespace, spc.Name) + } + + spcutil.BuildSecretObjectData(files, secretObj) + } + } + for _, secretObj := range spc.Spec.SecretObjects { secretName := strings.TrimSpace(secretObj.SecretName) @@ -543,8 +561,10 @@ func (r *Reconciler) patchSecret(ctx context.Context, name, namespace string, da if err != nil { return fmt.Errorf("failed to marshal old secret, err: %+v", err) } + secret.Data = data newData, err := json.Marshal(&newSecret) + if err != nil { return fmt.Errorf("failed to marshal new secret, err: %+v", err) } diff --git a/pkg/util/secretutil/secret.go b/pkg/util/secretutil/secret.go index 030d2239d..fbfa29196 100644 --- a/pkg/util/secretutil/secret.go +++ b/pkg/util/secretutil/secret.go @@ -31,7 +31,6 @@ import ( "sigs.k8s.io/secrets-store-csi-driver/apis/v1alpha1" corev1 "k8s.io/api/core/v1" - v1 "k8s.io/api/core/v1" ) const ( @@ -39,6 +38,8 @@ const ( privateKeyType = "PRIVATE KEY" privateKeyTypeRSA = "RSA PRIVATE KEY" privateKeyTypeEC = "EC PRIVATE KEY" + basicAuthUsername = "username" + basicAuthPassword = "password" ) // getCertPart returns the certificate or the private key part of the cert @@ -122,6 +123,21 @@ func getPrivateKey(data []byte) ([]byte, error) { return pem.EncodeToMemory(block), nil } +type basicAuthCreds struct { + Username string + Password string +} + +// getCredentials parses the mounted content and returns the required +// key-value pairs for a kubernetes.io/basic-auth K8s secret +func getCredentials(data []byte) basicAuthCreds { + credentials := strings.Split(string(data), ",") + return basicAuthCreds{ + Username: credentials[0], + Password: credentials[1], + } +} + // GetSecretType returns a k8s secret type. // Kubernetes doesn't impose any constraints on the type name: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types // If the secret type is empty, then default is Opaque. @@ -156,7 +172,7 @@ func GetSecretData(secretObjData []*v1alpha1.SecretObjectData, secretType corev1 dataKey := strings.TrimSpace(data.Key) if len(objectName) == 0 { - return datamap, fmt.Errorf("object name in secretObjects.data") + return datamap, fmt.Errorf("object name in secretObjects.data is empty") } if len(dataKey) == 0 { return datamap, fmt.Errorf("key in secretObjects.data is empty") @@ -170,13 +186,20 @@ func GetSecretData(secretObjData []*v1alpha1.SecretObjectData, secretType corev1 return datamap, fmt.Errorf("failed to read file %s, err: %v", objectName, err) } datamap[dataKey] = content - if secretType == v1.SecretTypeTLS { + if secretType == corev1.SecretTypeTLS { c, err := GetCertPart(content, dataKey) if err != nil { return datamap, fmt.Errorf("failed to get cert data from file %s, err: %+v", file, err) } datamap[dataKey] = c } + if secretType == corev1.SecretTypeBasicAuth { + credentials := getCredentials(content) + delete(datamap, dataKey) + + datamap[basicAuthUsername] = []byte(credentials.Username) + datamap[basicAuthPassword] = []byte(credentials.Password) + } } return datamap, nil } diff --git a/pkg/util/spcutil/secret_object_data.go b/pkg/util/spcutil/secret_object_data.go new file mode 100644 index 000000000..4dc5aa592 --- /dev/null +++ b/pkg/util/spcutil/secret_object_data.go @@ -0,0 +1,165 @@ +package spcutil + +import ( + "strings" + + "sigs.k8s.io/secrets-store-csi-driver/apis/v1alpha1" + + corev1 "k8s.io/api/core/v1" +) + +const ( + tlsKey = "tls.key" + tlsCert = "tls.crt" + dockerConfigJsonKey = ".dockerconfigjson" + sshPrivateKey = "ssh-privatekey" +) + +// BuildSecretObjectData builds the .Spec.SecretObjects[*].Data list of a SecretObject when SyncAll is true +func BuildSecretObjectData(files map[string]string, secretObj *v1alpha1.SecretObject) { + + for key := range files { + nested := strings.Split(key, "/") + var renamedKey string + + if len(nested) > 0 { + renamedKey = strings.Join(nested, "-") + } + + if renamedKey == "" { + secretObj.Data = append(secretObj.Data, &v1alpha1.SecretObjectData{ + ObjectName: key, + Key: key, + }) + continue + } + + secretObj.Data = append(secretObj.Data, &v1alpha1.SecretObjectData{ + ObjectName: key, + Key: renamedKey, + }) + } +} + +// BuildSecretObjects builds the .Spec.SecretObjects list of a SecretProviderClass when .SyncOptions.SyncAll is true +// How a SecretObject is built is dependent on the type of secret +func BuildSecretObjects(files map[string]string, secretType corev1.SecretType) []*v1alpha1.SecretObject { + secretObjects := []*v1alpha1.SecretObject{} + + var secretObject *v1alpha1.SecretObject + for key := range files { + + switch secretType { + case corev1.SecretTypeOpaque: + secretObject = createOpaqueSecretDataObject(key) + case corev1.SecretTypeTLS: + secretObject = createTLSSecretDataObject(key) + case corev1.SecretTypeDockerConfigJson: + secretObject = createDockerConfigJsonSecretDataObject(key) + case corev1.SecretTypeBasicAuth: + secretObject = createBasicAuthSecretDataObject(key) + case corev1.SecretTypeSSHAuth: + secretObject = createSSHSecretDataObject(key) + } + + secretObjects = append(secretObjects, secretObject) + } + + return secretObjects +} + +// createOpaqueSecretDataObject creates a SecretObject for an Opaque secret +func createOpaqueSecretDataObject(key string) *v1alpha1.SecretObject { + return &v1alpha1.SecretObject{ + SecretName: setSecretName(key), + Type: string(corev1.SecretTypeOpaque), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: key, + Key: setKey(key), + }, + }, + } +} + +// createTLSSecretDataObject creates a SecretObject for an TLS secret +func createTLSSecretDataObject(key string) *v1alpha1.SecretObject { + return &v1alpha1.SecretObject{ + SecretName: setSecretName(key), + Type: string(corev1.SecretTypeTLS), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: key, + Key: tlsKey, + }, + { + ObjectName: key, + Key: tlsCert, + }, + }, + } +} + +// createDockerConfigJsonSecretDataObject creates a SecretObject for an DockerConfigJSON secret +func createDockerConfigJsonSecretDataObject(key string) *v1alpha1.SecretObject { + return &v1alpha1.SecretObject{ + SecretName: setSecretName(key), + Type: string(corev1.SecretTypeDockerConfigJson), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: key, + Key: dockerConfigJsonKey, + }, + }, + } +} + +// createBasicAuthSecretDataObject creates a SecretObject for an Basic-Auth secret +func createBasicAuthSecretDataObject(key string) *v1alpha1.SecretObject { + return &v1alpha1.SecretObject{ + SecretName: setSecretName(key), + Type: string(corev1.SecretTypeBasicAuth), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: key, + Key: setKey(key), + }, + }, + } +} + +// createSSHSecretDataObject creates a SecretObject for an SSH-Auth secret +func createSSHSecretDataObject(key string) *v1alpha1.SecretObject { + return &v1alpha1.SecretObject{ + SecretName: setSecretName(key), + Type: string(corev1.SecretTypeSSHAuth), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: key, + Key: sshPrivateKey, + }, + }, + } +} + +// setSecretName sets the name of a secret to the value of "objectName" separated by "-" +func setSecretName(key string) string { + nested := strings.Split(key, "/") + + if len(nested) > 0 { + return strings.Join(nested, "-") + } + + return key +} + +// setKey sets the key of a secret to the name of the mounted file +func setKey(key string) string { + nested := strings.Split(key, "/") + + if len(nested) > 0 { + return nested[len(nested)-1] + } + + return key +} diff --git a/pkg/util/spcutil/secret_object_data_test.go b/pkg/util/spcutil/secret_object_data_test.go new file mode 100644 index 000000000..9418a82fe --- /dev/null +++ b/pkg/util/spcutil/secret_object_data_test.go @@ -0,0 +1,359 @@ +package spcutil + +import ( + "reflect" + "sort" + "testing" + + "sigs.k8s.io/secrets-store-csi-driver/apis/v1alpha1" + + corev1 "k8s.io/api/core/v1" +) + +const ( + cert = ` +-----BEGIN CERTIFICATE----- +MIIDOTCCAiGgAwIBAgIJAP0J5Z7N0Y5fMA0GCSqGSIb3DQEBCwUAMDMxFzAVBgNV +BAMMDmRlbW8uYXp1cmUuY29tMRgwFgYDVQQKDA9ha3MtaW5ncmVzcy10bHMwHhcN +MjAwNDE1MDQyMzQ2WhcNMjEwNDE1MDQyMzQ2WjAzMRcwFQYDVQQDDA5kZW1vLmF6 +dXJlLmNvbTEYMBYGA1UECgwPYWtzLWluZ3Jlc3MtdGxzMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAyS3Zky3n8JlLBxPLzgUpKZYxvzRadeWLmWVbK9by +o08S0Ss8Jao7Ay1wHtnLbn52rzCX6IX1sAe1TAT755Gk7JtLMkshtj6F8BNeelEy +E1gsBE5ntY5vyLTm/jZUIKz2Z9TLnqvQTmp6gJ68BKJ1NobnsHiAcKc6hI7kmY9C +oshmAi5qiKYBgzv/thji0093vtVSa9iwHhQp+AEIMhkvM5ZZkiU5eE6MT9SBEcVW +KmWF28UsB04daYwS2MKJ5l6d4n0LUdAG0FBt1lCoT9rwUDj9l3Mqmi953gw26LUr +NrYnM/8N2jl7Cuyw5alIWaUDrt5i+pu8wdWfzVk+fO7x8QIDAQABo1AwTjAdBgNV +HQ4EFgQUwFBbR014McETdrGGklpEQcl71Q0wHwYDVR0jBBgwFoAUwFBbR014McET +drGGklpEQcl71Q0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATgTy +gg1Q6ISSekiBCe12dqUTMFQh9GKpfYWKRbMtjOjpc7Mdwkdmm3Fu6l3RfEFT28Ij +fy97LMYv8W7beemDFqdmneb2w2ww0ZAFJg+GqIJZ9s/JadiFBDNU7CmJMhA225Qz +XC8ovejiePslnL4QJWlhVG93ZlBJ6SDkRgfcoIW2x4IBE6wv7jmRF4lOvb3z1ddP +iPQqhbEEbwMpXmWv7/2RnjAHdjdGaWRMC5+CaI+lqHyj6ir1c+e6u1QUY54qjmgM +koN/frqYab5Ek3kauj1iqW7rPkrFCqT2evh0YRqb1bFsCLJrRNxnOZ5wKXV/OYQa +QX5t0wFGCZ0KlbXDiw== +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJLdmTLefwmUsH +E8vOBSkpljG/NFp15YuZZVsr1vKjTxLRKzwlqjsDLXAe2ctufnavMJfohfWwB7VM +BPvnkaTsm0sySyG2PoXwE156UTITWCwETme1jm/ItOb+NlQgrPZn1Mueq9BOanqA +nrwEonU2hueweIBwpzqEjuSZj0KiyGYCLmqIpgGDO/+2GOLTT3e+1VJr2LAeFCn4 +AQgyGS8zllmSJTl4ToxP1IERxVYqZYXbxSwHTh1pjBLYwonmXp3ifQtR0AbQUG3W +UKhP2vBQOP2XcyqaL3neDDbotSs2ticz/w3aOXsK7LDlqUhZpQOu3mL6m7zB1Z/N +WT587vHxAgMBAAECggEAJb0qIYftCJ9ZCbzW8JDbRefc8SdbCN7Er0PqNHEgFy6Q +MxjPMambZF8ztzXYCaRDk12kQYRPsHPhuJ7+ulQCAjinhIm/izZzXbPkd0GgCSzz +JOOoZNCRe68j3fBHG9IWbyfmAp/sdalXzaT5VE09e7sW323bekaEnbVIgN30/CAS +gI77YdaIhG+PT/pSCOc11MTkBJp+VhT1tEtlRAR78b1RXbGi1oUHRee7C3Ia8IKQ +3L5dPxR9RsYsR2O66908kEi8ZcuIjcbIuRPDXYHY+5Nwm3mXuZlkyjyfxJXsIA8i +qBrQrSpHGgAn1TVlLDSCKPLbkRzBRRvAW0zL/cDTuQKBgQDq/9Yxx9QivAuUxxdE +u0VO5CzzZYFWhDxAXS3/wYyo1YnoPtUz/lGCvMWp0k2aaa0+KTXv2fRCUGSujHW7 +Jfo4kuMPkauAhoXx9QJAcjoK0nNbYEaqoJyMoRID+Qb9XHkj+lmBTmMVgALCT9DI +HekHj/M3b7CknbfWv1sOZ/vpQwKBgQDbKEuP/DWQa9DC5nn5phHD/LWZLG/cMR4X +TmwM/cbfRxM/6W0+/KLAodz4amGRzVlW6ax4k26BSE8Zt/SiyA1DQRTeFloduoqW +iWF4dMeItxw2am+xLREwtoN3FgsJHu2z/O/0aaBAOMLUXIPIyiE4L6OnEPifE/pb +AM8EbM5auwKBgGhdABIRjbtzSa1kEYhbprcXjIL3lE4I4f0vpIsNuNsOInW62dKC +Yk6uaRY3KHGn9uFBSgvf/qMost310R8xCYPwb9htN/4XQAspZTubvv0pY0O0aQ3D +0GJ/8dFD2f/Q/pekyfUsC8Lzm8YRzkXhSqkqG7iF6Kviw08iolyuf2ijAoGBANaA +pRzDvWWisUziKsa3zbGnGdNXVBEPniUvo8A/b7RAK84lWcEJov6qLs6RyPfdJrFT +u3S00LcHICzLCU1+QsTt4U/STtfEKjtXMailnFrq5lk4aiPfOXEVYq1fTOPbesrt +Katu6uOQ6tjRyEbx1/vXXPV7Peztr9/8daMeIAdbAoGBAOYRJ1CzMYQKjWF32Uas +7hhQxyH1QI4nV56Dryq7l/UWun2pfwNLZFqOHD3qm05aznzNKvk9aHAsOPFfUUXO +7sp0Ge5FLMSw1uMNnutcVcMz37KAY2fOoE2xoLM4DU/H2NqDjeGCsOsU1ReRS1vB +J+42JGwBdLV99ruYKVKOWPh4 +-----END PRIVATE KEY----- +` + + sshKey = ` +-----BEGIN OPENSSH PRIVATE KEY----- +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 +C9I7vbQHcdYGWqof7MBPsMcaJ+0= +-----END OPENSSH PRIVATE KEY----- + +` + + dockerconfigjson = "{\"auths\":{\"https://index.docker.io/v1/\":{\"username\":\"user1\",\"password\":\"password1\",\"email\":\"account1@email.com\",\"auth\":\"dXNlcjE6cGFzc3dvcmQx\"}}}" +) + +func TestBuildSecretObjectData(t *testing.T) { + files := map[string]string{ + "username": "test user", + "password": "test password", + "nested/username": "test nested user", + "nested/double/username": "double nested user", + } + + secretObj := &v1alpha1.SecretObject{ + SecretName: "test-secret", + Type: "Opaque", + SyncAll: true, + } + + BuildSecretObjectData(files, secretObj) + + expected := &v1alpha1.SecretObject{ + SecretName: "test-secret", + Type: "Opaque", + SyncAll: true, + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: "username", + Key: "username", + }, + { + ObjectName: "password", + Key: "password", + }, + { + ObjectName: "nested/username", + Key: "nested-username", + }, + { + ObjectName: "nested/double/username", + Key: "nested-double-username", + }, + }, + } + + if ok := assertSecretsObjectsEqual(expected, secretObj); !ok { + t.Fatal("secret objects did not match") + } +} + +func TestBuildSecretObjects(t *testing.T) { + tests := []struct { + files map[string]string + secretType corev1.SecretType + expected []*v1alpha1.SecretObject + }{ + { + files: map[string]string{ + "username": "test user", + "password": "test password", + "nested/username": "test nested user", + "nested/double/username": "double nested user", + }, + secretType: corev1.SecretTypeOpaque, + expected: []*v1alpha1.SecretObject{ + { + SecretName: "username", + Type: string(corev1.SecretTypeOpaque), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: "username", + Key: "username", + }, + }, + }, + { + SecretName: "password", + Type: string(corev1.SecretTypeOpaque), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: "password", + Key: "password", + }, + }, + }, + { + SecretName: "nested-username", + Type: string(corev1.SecretTypeOpaque), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: "nested/username", + Key: "username", + }, + }, + }, + { + SecretName: "nested-double-username", + Type: string(corev1.SecretTypeOpaque), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: "nested/double/username", + Key: "username", + }, + }, + }, + }, + }, { + files: map[string]string{ + "cert": cert, + }, + secretType: corev1.SecretTypeTLS, + expected: []*v1alpha1.SecretObject{ + { + SecretName: "cert", + Type: string(corev1.SecretTypeTLS), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: "cert", + Key: "tls.key", + }, + { + ObjectName: "cert", + Key: "tls.crt", + }, + }, + }, + }, + }, { + files: map[string]string{ + "basic/basic1": "my-username1,my-password1", + "basic/basic2": "my-username2,my-password2", + "basic/basic3": "my-username3,my-password3", + }, + secretType: corev1.SecretTypeBasicAuth, + expected: []*v1alpha1.SecretObject{ + { + SecretName: "basic-basic1", + Type: string(corev1.SecretTypeBasicAuth), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: "basic/basic1", + Key: "basic1", + }, + }, + }, + { + SecretName: "basic-basic2", + Type: string(corev1.SecretTypeBasicAuth), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: "basic/basic2", + Key: "basic2", + }, + }, + }, + { + SecretName: "basic-basic3", + Type: string(corev1.SecretTypeBasicAuth), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: "basic/basic3", + Key: "basic3", + }, + }, + }, + }, + }, { + files: map[string]string{ + "ssh": sshKey, + }, + secretType: corev1.SecretTypeSSHAuth, + expected: []*v1alpha1.SecretObject{ + { + SecretName: "ssh", + Type: string(corev1.SecretTypeSSHAuth), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: "ssh", + Key: "ssh-privatekey", + }, + }, + }, + }, + }, + { + files: map[string]string{ + "dev/docker": dockerconfigjson, + }, + secretType: corev1.SecretTypeDockerConfigJson, + expected: []*v1alpha1.SecretObject{ + { + SecretName: "dev-docker", + Type: string(corev1.SecretTypeDockerConfigJson), + Data: []*v1alpha1.SecretObjectData{ + { + ObjectName: "dev/docker", + Key: ".dockerconfigjson", + }, + }, + }, + }, + }, + } + + for _, test := range tests { + actualSecretObjects := BuildSecretObjects(test.files, test.secretType) + if ok := assertSecretObjectSlicesEqual(test.expected, actualSecretObjects); !ok { + t.Fatal("secret object slices did not match") + } + } + +} + +func assertSecretsObjectsEqual(expected, actual *v1alpha1.SecretObject) bool { + if expected.SecretName != actual.SecretName { + return false + } + + if expected.Type != actual.Type { + return false + } + + sort.Slice(expected.Data, func(i, j int) bool { + return expected.Data[i].ObjectName < expected.Data[j].ObjectName + }) + + sort.Slice(actual.Data, func(i, j int) bool { + return actual.Data[i].ObjectName < actual.Data[j].ObjectName + }) + + return reflect.DeepEqual(expected.Data, actual.Data) +} + +func assertSecretObjectSlicesEqual(expected, actual []*v1alpha1.SecretObject) bool { + if len(expected) != len(actual) { + return false + } + + sort.Slice(expected, func(i, j int) bool { + return expected[i].SecretName < expected[j].SecretName + }) + + sort.Slice(actual, func(i, j int) bool { + return actual[i].SecretName < actual[j].SecretName + }) + + for i := 0; i < len(expected); i++ { + if ok := assertSecretsObjectsEqual(expected[i], actual[i]); !ok { + return false + } + } + + return true +} diff --git a/test/bats/tests/vault/deployment-three-synck8s.yaml b/test/bats/tests/vault/deployment-three-synck8s.yaml new file mode 100644 index 000000000..fa95df064 --- /dev/null +++ b/test/bats/tests/vault/deployment-three-synck8s.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: busybox-deployment-three + labels: + app: busybox-sync-all +spec: + replicas: 2 + selector: + matchLabels: + app: busybox-sync-all + template: + metadata: + labels: + app: busybox-sync-all + spec: + terminationGracePeriodSeconds: 0 + containers: + - image: k8s.gcr.io/e2e-test-images/busybox:1.29 + name: busybox-sync-all + imagePullPolicy: IfNotPresent + command: + - "/bin/sleep" + - "10000" + env: + - name: SECRET_USERNAME + valueFrom: + secretKeyRef: + name: foosecret + key: bar + - name: SECRET_PASSWORD + valueFrom: + secretKeyRef: + name: foosecret + key: bar1 + volumeMounts: + - name: secrets-store-inline + mountPath: "/mnt/secrets-store" + readOnly: true + volumes: + - name: secrets-store-inline + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: "vault-foo-sync-all" diff --git a/test/bats/tests/vault/vault_synck8s_syncall_v1alpha1_secretproviderclass.yaml b/test/bats/tests/vault/vault_synck8s_syncall_v1alpha1_secretproviderclass.yaml new file mode 100644 index 000000000..1d3a74139 --- /dev/null +++ b/test/bats/tests/vault/vault_synck8s_syncall_v1alpha1_secretproviderclass.yaml @@ -0,0 +1,25 @@ +apiVersion: secrets-store.csi.x-k8s.io/v1alpha1 +kind: SecretProviderClass +metadata: + name: vault-foo-sync-all +spec: + provider: vault + secretObjects: + - secretName: foosecret + syncAll: true + type: Opaque + labels: + environment: "test" + parameters: + roleName: "csi" + vaultAddress: "http://vault.vault:8200" + objects: | + - secretPath: "secret/data/foo" + objectName: "bar" + secretKey: "bar" + - secretPath: "secret/data/foo1" + objectName: "bar1" + secretKey: "bar1" + - secretPath: "secret/data/foo" + objectName: "nested/bar" + secretKey: "bar" \ No newline at end of file diff --git a/test/bats/vault.bats b/test/bats/vault.bats index 9b3c14b88..70d3d86f6 100644 --- a/test/bats/vault.bats +++ b/test/bats/vault.bats @@ -206,6 +206,66 @@ EOF assert_success } +@test "Sync with K8s secrets using SyncAll - create deployment" { + kubectl apply -f $BATS_TESTS_DIR/vault_synck8s_syncall_v1alpha1_secretproviderclass.yaml + kubectl wait --for condition=established --timeout=60s crd/secretproviderclasses.secrets-store.csi.x-k8s.io + + cmd="kubectl get secretproviderclasses.secrets-store.csi.x-k8s.io/vault-foo-sync-all -o yaml | grep vault" + wait_for_process $WAIT_TIME $SLEEP_TIME "$cmd" + + run kubectl apply -f $BATS_TESTS_DIR/deployment-three-synck8s.yaml + assert_success + + kubectl wait --for=condition=Ready --timeout=120s pod -l app=busybox-sync-all +} + +@test "Sync with K8s secrets using SyncAll - read secret from pod, read K8s secret, read env var, check secret ownerReferences with multiple owners" { + POD=$(kubectl get pod -l app=busybox-sync-all -o jsonpath="{.items[0].metadata.name}") + result=$(kubectl exec $POD -- cat /mnt/secrets-store/bar) + [[ "$result" == "hello" ]] + + result=$(kubectl exec $POD -- cat /mnt/secrets-store/bar1) + [[ "$result" == "hello1" ]] + + result=$(kubectl exec $POD -- cat /mnt/secrets-store/nested/bar) + [[ "$result" == "hello" ]] + + result=$(kubectl get secret foosecret -o jsonpath="{.data.bar}" | base64 -d) + [[ "$result" == "hello" ]] + + result=$(kubectl get secret foosecret -o jsonpath="{.data.bar1}" | base64 -d) + [[ "$result" == "hello1" ]] + + result=$(kubectl get secret foosecret -o jsonpath="{.data.nested-bar}" | base64 -d) + [[ "$result" == "hello" ]] + + result=$(kubectl exec $POD -- printenv | grep SECRET_USERNAME | awk -F"=" '{ print $2 }' | tr -d '\r\n') + [[ "$result" == "hello" ]] + + result=$(kubectl exec $POD -- printenv | grep SECRET_PASSWORD | awk -F"=" '{ print $2 }' | tr -d '\r\n') + [[ "$result" == "hello1" ]] + + result=$(kubectl get secret foosecret -o jsonpath="{.metadata.labels.environment}") + [[ "${result//$'\r'}" == "${LABEL_VALUE}" ]] + + result=$(kubectl get secret foosecret -o jsonpath="{.metadata.labels.secrets-store\.csi\.k8s\.io/managed}") + [[ "${result//$'\r'}" == "true" ]] + + run wait_for_process $WAIT_TIME $SLEEP_TIME "compare_owner_count foosecret default 1" + assert_success +} + +@test "Sync with K8s secrets using SyncAll - delete deployment, check secret is deleted" { + run kubectl delete -f $BATS_TESTS_DIR/deployment-three-synck8s.yaml + assert_success + + run wait_for_process $WAIT_TIME $SLEEP_TIME "check_secret_deleted foosecret default" + assert_success + + run kubectl delete -f $BATS_TESTS_DIR/vault_synck8s_syncall_v1alpha1_secretproviderclass.yaml + assert_success +} + @test "Test Namespaced scope SecretProviderClass - create deployment" { kubectl create ns test-ns