diff --git a/README.md b/README.md index 7873585..04d8263 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ The parameters are summarized in the table below : | `path-deletion` | false | - | no | Trigger path deletion on the S3 backend upon CR deletion. Limited to deleting the `.keep` files used by the operator. | | `s3User-deletion` | false | - | no | Trigger S3User deletion on the S3 backend upon CR deletion. | | `override-existing-secret` | false | - | no | Update secret linked to s3User if already exist, else noop | -| `s3LabelSelector` | "" | - | no | Filter resource that this instance will manage. If Empty all resource in the cluster will be manage | +| `allowedNamespaces` | "" | - | no | namespace allowed to use default s3Instance | ## Minimal rights needed to work The Operator need at least this rights: @@ -170,6 +170,7 @@ spec: secretName: minio-credentials # Name of the secret containing 2 Keys S3_ACCESS_KEY and S3_SECRET_KEY region: us-east-1 # Region of the Provider useSSL: true # useSSL to query the Provider + allowedNamespaces: [] # AllowedNamespaces to use this s3instance can get regexp if empty only the same namespace as s3instance is allowed ``` ### Bucket example @@ -307,6 +308,13 @@ spec: Each S3user is linked to a kubernetes secret which have the same name that the S3User. The secret contains 2 keys: `accessKey` and `secretKey`. +### :info: How works s3InstanceRef + +S3InstanceRef can get the following values: +- empty: In this case the s3instance use will be the default one configured at startup if the namespace is in the namespace allowed for this s3Instance +- `s3InstanceName`: In this case the s3Instance use will be the s3Instance with the name `s3InstanceName` in the current namespace (if the current namespace is allowed) +- `namespace/s3InstanceName`: In this case the s3Instance use will be the s3Instance with the name `s3InstanceName` in the namespace `namespace` (if the current namespace is allowed to use this s3Instance) + ## Operator SDK generated guidelines
diff --git a/api/v1alpha1/bucket_types.go b/api/v1alpha1/bucket_types.go index 7f15adb..b258bb3 100644 --- a/api/v1alpha1/bucket_types.go +++ b/api/v1alpha1/bucket_types.go @@ -38,6 +38,7 @@ type BucketSpec struct { // s3InstanceRef where create the bucket // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="S3InstanceRef is immutable" S3InstanceRef string `json:"s3InstanceRef,omitempty"` // Quota to apply to the bucket diff --git a/api/v1alpha1/path_types.go b/api/v1alpha1/path_types.go index 45c7ce9..019d7ef 100644 --- a/api/v1alpha1/path_types.go +++ b/api/v1alpha1/path_types.go @@ -38,6 +38,7 @@ type PathSpec struct { // s3InstanceRef where create the Paths // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="S3InstanceRef is immutable" S3InstanceRef string `json:"s3InstanceRef,omitempty"` } diff --git a/api/v1alpha1/policy_types.go b/api/v1alpha1/policy_types.go index ac7a07c..2825b45 100644 --- a/api/v1alpha1/policy_types.go +++ b/api/v1alpha1/policy_types.go @@ -38,6 +38,7 @@ type PolicySpec struct { // s3InstanceRef where create the Policy // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="S3InstanceRef is immutable" S3InstanceRef string `json:"s3InstanceRef,omitempty"` } diff --git a/api/v1alpha1/s3instance_types.go b/api/v1alpha1/s3instance_types.go index eb03eff..2edcaa5 100644 --- a/api/v1alpha1/s3instance_types.go +++ b/api/v1alpha1/s3instance_types.go @@ -46,9 +46,13 @@ type S3InstanceSpec struct { // +kubebuilder:validation:Optional UseSSL bool `json:"useSSL,omitempty"` - // CaCertificatesBase64 associated to the S3InstanceUrl + // Secret containing key ca.crt with the certificate associated to the S3InstanceUrl // +kubebuilder:validation:Optional - CaCertificatesBase64 []string `json:"caCertificateBase64,omitempty"` + CaCertSecretRef string `json:"caCertSecretRef,omitempty"` + + // AllowedNamespaces to use this S3InstanceUrl if empty only the namespace of this instance url is allowed to use it + // +kubebuilder:validation:Optional + AllowedNamespaces []string `json:"allowedNamespaces,omitempty"` } // S3InstanceStatus defines the observed state of S3Instance diff --git a/api/v1alpha1/s3user_types.go b/api/v1alpha1/s3user_types.go index e116a92..be69923 100644 --- a/api/v1alpha1/s3user_types.go +++ b/api/v1alpha1/s3user_types.go @@ -40,6 +40,7 @@ type S3UserSpec struct { // s3InstanceRef where create the user // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="S3InstanceRef is immutable" S3InstanceRef string `json:"s3InstanceRef,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 46c7fe1..f9ca52d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -402,8 +402,8 @@ func (in *S3InstanceList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *S3InstanceSpec) DeepCopyInto(out *S3InstanceSpec) { *out = *in - if in.CaCertificatesBase64 != nil { - in, out := &in.CaCertificatesBase64, &out.CaCertificatesBase64 + if in.AllowedNamespaces != nil { + in, out := &in.AllowedNamespaces, &out.AllowedNamespaces *out = make([]string, len(*in)) copy(*out, *in) } diff --git a/config/crd/bases/s3.onyxia.sh_buckets.yaml b/config/crd/bases/s3.onyxia.sh_buckets.yaml index 098120e..9973ea4 100644 --- a/config/crd/bases/s3.onyxia.sh_buckets.yaml +++ b/config/crd/bases/s3.onyxia.sh_buckets.yaml @@ -60,6 +60,9 @@ spec: s3InstanceRef: description: s3InstanceRef where create the bucket type: string + x-kubernetes-validations: + - message: S3InstanceRef is immutable + rule: self == oldSelf required: - name - quota diff --git a/config/crd/bases/s3.onyxia.sh_paths.yaml b/config/crd/bases/s3.onyxia.sh_paths.yaml index c124fd0..44778b0 100644 --- a/config/crd/bases/s3.onyxia.sh_paths.yaml +++ b/config/crd/bases/s3.onyxia.sh_paths.yaml @@ -46,6 +46,9 @@ spec: s3InstanceRef: description: s3InstanceRef where create the Paths type: string + x-kubernetes-validations: + - message: S3InstanceRef is immutable + rule: self == oldSelf required: - bucketName type: object diff --git a/config/crd/bases/s3.onyxia.sh_policies.yaml b/config/crd/bases/s3.onyxia.sh_policies.yaml index aaa69a1..6c422ef 100644 --- a/config/crd/bases/s3.onyxia.sh_policies.yaml +++ b/config/crd/bases/s3.onyxia.sh_policies.yaml @@ -44,6 +44,9 @@ spec: s3InstanceRef: description: s3InstanceRef where create the Policy type: string + x-kubernetes-validations: + - message: S3InstanceRef is immutable + rule: self == oldSelf required: - name - policyContent diff --git a/config/crd/bases/s3.onyxia.sh_s3instances.yaml b/config/crd/bases/s3.onyxia.sh_s3instances.yaml index e1654f5..5cf716b 100644 --- a/config/crd/bases/s3.onyxia.sh_s3instances.yaml +++ b/config/crd/bases/s3.onyxia.sh_s3instances.yaml @@ -35,11 +35,16 @@ spec: spec: description: S3InstanceSpec defines the desired state of S3Instance properties: - caCertificateBase64: - description: CaCertificatesBase64 associated to the S3InstanceUrl + allowedNamespaces: + description: AllowedNamespaces to use this S3InstanceUrl if empty + only the namespace of this instance url is allowed to use it items: type: string type: array + caCertSecretRef: + description: Secret containing key ca.crt with the certificate associated + to the S3InstanceUrl + type: string region: description: region associated to the S3Instance type: string diff --git a/config/crd/bases/s3.onyxia.sh_s3users.yaml b/config/crd/bases/s3.onyxia.sh_s3users.yaml index 17b96cf..b3e42d3 100644 --- a/config/crd/bases/s3.onyxia.sh_s3users.yaml +++ b/config/crd/bases/s3.onyxia.sh_s3users.yaml @@ -46,6 +46,9 @@ spec: s3InstanceRef: description: s3InstanceRef where create the user type: string + x-kubernetes-validations: + - message: S3InstanceRef is immutable + rule: self == oldSelf secretName: description: SecretName associated to the S3User type: string diff --git a/controllers/bucket_controller.go b/controllers/bucket_controller.go index 5e88562..2a24546 100644 --- a/controllers/bucket_controller.go +++ b/controllers/bucket_controller.go @@ -19,6 +19,7 @@ package controllers import ( "context" "fmt" + "strings" "time" s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" @@ -74,19 +75,6 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } - // check if this object must be manage by this instance - if r.S3LabelSelectorValue != "" { - labelSelectorValue, found := bucketResource.Labels[utils.S3OperatorBucketLabelSelectorKey] - if !found { - logger.Info("This bucket ressouce will not be manage by this instance because this instance require that Bucket get labelSelector and label selector not found", "req.Name", req.Name, "Bucket Labels", bucketResource.Labels, "S3OperatorBucketLabelSelectorKey", utils.S3OperatorBucketLabelSelectorKey) - return ctrl.Result{}, nil - } - if labelSelectorValue != r.S3LabelSelectorValue { - logger.Info("This bucket ressouce will not be manage by this instance because this instance require that Bucket get specific a specific labelSelector value", "req.Name", req.Name, "expected", r.S3LabelSelectorValue, "current", labelSelectorValue) - return ctrl.Result{}, nil - } - } - // Managing bucket deletion with a finalizer // REF : https://sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#external-resources isMarkedForDeletion := bucketResource.GetDeletionTimestamp() != nil @@ -131,9 +119,15 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // Create S3Client s3Client, err := r.getS3InstanceForObject(ctx, bucketResource) if err != nil { - logger.Error(err, "an error occurred while getting s3Client") - return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", - "Getting s3Client in cache has failed", err) + if customErr, ok := err.(*s3ClientCache.S3ClientNotFound); ok { + logger.Error(err, "an error occurred while getting s3Client") + return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + customErr.Reason, err) + } else { + logger.Error(err, "an error occurred while getting s3Client") + return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + "Unknown error occured while getting bucket", err) + } } // Bucket lifecycle management (other than deletion) starts here @@ -298,23 +292,40 @@ func (r *BucketReconciler) SetBucketStatusConditionAndUpdate(ctx context.Context func (r *BucketReconciler) getS3InstanceForObject(ctx context.Context, bucketResource *s3v1alpha1.Bucket) (factory.S3Client, error) { logger := log.FromContext(ctx) if bucketResource.Spec.S3InstanceRef == "" { - logger.Info("Bucket resource doesn't have S3InstanceRef fill, failback to default instance") + logger.Info("Bucket resource doesn't refer to s3Instance, failback to default one") s3Client, found := r.S3ClientCache.Get("default") if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: "No default client was found"} - logger.Error(err, "No default client was found") + err := &s3ClientCache.S3ClientNotFound{Reason: "Client not found"} + logger.Error(err, "Client \"default\" was not found") return nil, err + } else { + if utils.IsAllowedNamespaces(bucketResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientNotFound{Reason: "Client \"default\" was not found"} + return nil, err + } } - return s3Client, nil } else { - - logger.Info(fmt.Sprintf("Bucket resource refer to s3Instance: %s, search instance in cache", bucketResource.Spec.S3InstanceRef)) - s3Client, found := r.S3ClientCache.Get(bucketResource.Spec.S3InstanceRef) + logger.Info(fmt.Sprintf("Bucket resource doesn't refer to s3Instance: %s, search instance in cache", bucketResource.Spec.S3InstanceRef)) + clientName := "" + if strings.Contains(bucketResource.Spec.S3InstanceRef, "/") { + clientName = bucketResource.Spec.S3InstanceRef + } else { + clientName = bucketResource.Namespace + "/" + bucketResource.Spec.S3InstanceRef + } + s3Client, found := r.S3ClientCache.Get(clientName) if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s, not found in cache", bucketResource.Spec.S3InstanceRef)} - logger.Error(err, "No client was found") + err := &s3ClientCache.S3ClientNotFound{Reason: fmt.Sprintf("S3InstanceRef: %s not found in cache", clientName)} + logger.Error(err, fmt.Sprintf("S3InstanceRef: %s not found in cache", clientName)) + return nil, err + } + logger.Info(fmt.Sprintf("Check if BucketRessource %s can use S3Instance %s", bucketResource.Name, clientName)) + if utils.IsAllowedNamespaces(bucketResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientNotFound{Reason: fmt.Sprintf("Client %s is not allowed in this namespace", bucketResource.Spec.S3InstanceRef)} return nil, err } - return s3Client, nil } } diff --git a/controllers/path_controller.go b/controllers/path_controller.go index f05549a..0c04a29 100644 --- a/controllers/path_controller.go +++ b/controllers/path_controller.go @@ -19,6 +19,7 @@ package controllers import ( "context" "fmt" + "strings" "time" s3ClientCache "github.com/InseeFrLab/s3-operator/internal/s3" @@ -74,19 +75,6 @@ func (r *PathReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. return ctrl.Result{}, err } - // check if this object must be manage by this instance - if r.S3LabelSelectorValue != "" { - labelSelectorValue, found := pathResource.Labels[utils.S3OperatorPathLabelSelectorKey] - if !found { - logger.Info("This paht ressouce will not be manage by this instance because this instance require that path get labelSelector and label selector not found", "req.Name", req.Name, "Bucket Labels", pathResource.Labels, "S3OperatorBucketLabelSelectorKey", utils.S3OperatorBucketLabelSelectorKey) - return ctrl.Result{}, nil - } - if labelSelectorValue != r.S3LabelSelectorValue { - logger.Info("This path ressouce will not be manage by this instance because this instance require that path get specific a specific labelSelector value", "req.Name", req.Name, "expected", r.S3LabelSelectorValue, "current", labelSelectorValue) - return ctrl.Result{}, nil - } - } - // Managing path deletion with a finalizer // REF : https://sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#external-resources isMarkedForDeletion := pathResource.GetDeletionTimestamp() != nil @@ -132,9 +120,15 @@ func (r *PathReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. // Create S3Client s3Client, err := r.getS3InstanceForObject(ctx, pathResource) if err != nil { - logger.Error(err, "an error occurred while getting s3Client") - return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", - "Getting s3Client in cache has failed", err) + if customErr, ok := err.(*s3ClientCache.S3ClientNotFound); ok { + logger.Error(err, "an error occurred while getting s3Client") + return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + customErr.Reason, err) + } else { + logger.Error(err, "an error occurred while getting s3Client") + return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + "Unknown error occured while getting bucket", err) + } } // Path lifecycle management (other than deletion) starts here @@ -264,23 +258,40 @@ func (r *PathReconciler) SetPathStatusConditionAndUpdate(ctx context.Context, pa func (r *PathReconciler) getS3InstanceForObject(ctx context.Context, pathResource *s3v1alpha1.Path) (factory.S3Client, error) { logger := log.FromContext(ctx) if pathResource.Spec.S3InstanceRef == "" { - logger.Info("Bucket resource doesn't refer to s3Instance, failback to default one") + logger.Info("Path resource doesn't refer to s3Instance, failback to default one") s3Client, found := r.S3ClientCache.Get("default") if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: "No default client was found"} - logger.Error(err, "No default client was found") + err := &s3ClientCache.S3ClientNotFound{Reason: "Client not found"} + logger.Error(err, "Client \"default\" was not found") return nil, err + } else { + if utils.IsAllowedNamespaces(pathResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientNotFound{Reason: "Client \"default\" was not found"} + return nil, err + } } - return s3Client, nil } else { - - logger.Info(fmt.Sprintf("Bucket resource doesn't refer to s3Instance: %s, search instance in cache", pathResource.Spec.S3InstanceRef)) - s3Client, found := r.S3ClientCache.Get(pathResource.Spec.S3InstanceRef) + logger.Info(fmt.Sprintf("Path resource doesn't refer to s3Instance: %s, search instance in cache", pathResource.Spec.S3InstanceRef)) + clientName := "" + if strings.Contains(pathResource.Spec.S3InstanceRef, "/") { + clientName = pathResource.Spec.S3InstanceRef + } else { + clientName = pathResource.Namespace + "/" + pathResource.Spec.S3InstanceRef + } + s3Client, found := r.S3ClientCache.Get(clientName) if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache", pathResource.Spec.S3InstanceRef)} - logger.Error(err, "No client was found") + err := &s3ClientCache.S3ClientNotFound{Reason: fmt.Sprintf("S3InstanceRef: %s not found in cache", clientName)} + logger.Error(err, fmt.Sprintf("S3InstanceRef: %s not found in cache", clientName)) + return nil, err + } + logger.Info(fmt.Sprintf("Check if PathRessource %s can use S3Instance %s", pathResource.Name, clientName)) + if utils.IsAllowedNamespaces(pathResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientNotFound{Reason: fmt.Sprintf("Client %s is not allowed in this namespace", pathResource.Spec.S3InstanceRef)} return nil, err } - return s3Client, nil } } diff --git a/controllers/policy_controller.go b/controllers/policy_controller.go index 37c3ee8..b214d03 100644 --- a/controllers/policy_controller.go +++ b/controllers/policy_controller.go @@ -21,6 +21,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "time" "github.com/minio/madmin-go/v3" @@ -77,19 +78,6 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } - // check if this object must be manage by this instance - if r.S3LabelSelectorValue != "" { - labelSelectorValue, found := policyResource.Labels[utils.S3OperatorPolicyLabelSelectorKey] - if !found { - logger.Info("This policy ressouce will not be manage by this instance because this instance require that policy get labelSelector and label selector not found", "req.Name", req.Name, "Policy Labels", policyResource.Labels, "S3OperatorPolicyLabelSelectorKey", utils.S3OperatorPolicyLabelSelectorKey) - return ctrl.Result{}, nil - } - if labelSelectorValue != r.S3LabelSelectorValue { - logger.Info("This policy ressouce will not be manage by this instance because this instance require that policy get specific a specific labelSelector value", "req.Name", req.Name, "expected", r.S3LabelSelectorValue, "current", labelSelectorValue) - return ctrl.Result{}, nil - } - } - // Managing policy deletion with a finalizer // REF : https://sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#external-resources isMarkedForDeletion := policyResource.GetDeletionTimestamp() != nil @@ -137,9 +125,15 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // Create S3Client s3Client, err := r.getS3InstanceForObject(ctx, policyResource) if err != nil { - logger.Error(err, "an error occurred while getting s3Client") - return r.SetPolicyStatusConditionAndUpdate(ctx, policyResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", - "Getting s3Client in cache has failed", err) + if customErr, ok := err.(*s3ClientCache.S3ClientNotFound); ok { + logger.Error(err, "an error occurred while getting s3Client") + return r.SetPolicyStatusConditionAndUpdate(ctx, policyResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + customErr.Reason, err) + } else { + logger.Error(err, "an error occurred while getting s3Client") + return r.SetPolicyStatusConditionAndUpdate(ctx, policyResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + "Unknown error occured while getting bucket", err) + } } // Check policy existence on the S3 server @@ -272,22 +266,40 @@ func (r *PolicyReconciler) SetPolicyStatusConditionAndUpdate(ctx context.Context func (r *PolicyReconciler) getS3InstanceForObject(ctx context.Context, policyResource *s3v1alpha1.Policy) (factory.S3Client, error) { logger := log.FromContext(ctx) if policyResource.Spec.S3InstanceRef == "" { - logger.Info("Bucket resource doesn't refer to s3Instance, failback to default one") + logger.Info("Policy resource doesn't refer to s3Instance, failback to default one") s3Client, found := r.S3ClientCache.Get("default") if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: "No default client was found"} - logger.Error(err, "No default client was found") + err := &s3ClientCache.S3ClientNotFound{Reason: "Client not found"} + logger.Error(err, "Client \"default\" was not found") return nil, err + } else { + if utils.IsAllowedNamespaces(policyResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientNotFound{Reason: "Client \"default\" was not found"} + return nil, err + } } - return s3Client, nil } else { - logger.Info(fmt.Sprintf("Bucket resource doesn't refer to s3Instance: %s, search instance in cache", policyResource.Spec.S3InstanceRef)) - s3Client, found := r.S3ClientCache.Get(policyResource.Spec.S3InstanceRef) + logger.Info(fmt.Sprintf("Policy resource doesn't refer to s3Instance: %s, search instance in cache", policyResource.Spec.S3InstanceRef)) + clientName := "" + if strings.Contains(policyResource.Spec.S3InstanceRef, "/") { + clientName = policyResource.Spec.S3InstanceRef + } else { + clientName = policyResource.Namespace + "/" + policyResource.Spec.S3InstanceRef + } + s3Client, found := r.S3ClientCache.Get(clientName) if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache", policyResource.Spec.S3InstanceRef)} - logger.Error(err, "No client was found") + err := &s3ClientCache.S3ClientNotFound{Reason: fmt.Sprintf("S3InstanceRef: %s not found in cache", clientName)} + logger.Error(err, fmt.Sprintf("S3InstanceRef: %s not found in cache", clientName)) + return nil, err + } + logger.Info(fmt.Sprintf("Check if PolicyRessource %s can use S3Instance %s", policyResource.Name, clientName)) + if utils.IsAllowedNamespaces(policyResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientNotFound{Reason: fmt.Sprintf("Client %s is not allowed in this namespace", policyResource.Spec.S3InstanceRef)} return nil, err } - return s3Client, nil } } diff --git a/controllers/s3instance_controller.go b/controllers/s3instance_controller.go index c46f1b5..a1d6b23 100644 --- a/controllers/s3instance_controller.go +++ b/controllers/s3instance_controller.go @@ -79,19 +79,6 @@ func (r *S3InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, err } - // check if this object must be manage by this instance - if r.S3LabelSelectorValue != "" { - labelSelectorValue, found := s3InstanceResource.Labels[utils.S3OperatorS3InstanceLabelSelectorKey] - if !found { - logger.Info("This s3Instance ressouce will not be manage by this instance because this instance require that s3Instance get labelSelector and label selector not found", "req.Name", req.Name, "Bucket Labels", s3InstanceResource.Labels, "S3OperatorBucketLabelSelectorKey", utils.S3OperatorBucketLabelSelectorKey) - return ctrl.Result{}, nil - } - if labelSelectorValue != r.S3LabelSelectorValue { - logger.Info("This s3Instance ressouce will not be manage by this instance because this instance require that s3Instance get specific a specific labelSelector value", "req.Name", req.Name, "expected", r.S3LabelSelectorValue, "current", labelSelectorValue) - return ctrl.Result{}, nil - } - } - // Check if the s3InstanceResource instance is marked to be deleted, which is // indicated by the deletion timestamp being set. The object will be deleted. if s3InstanceResource.GetDeletionTimestamp() != nil { @@ -113,7 +100,7 @@ func (r *S3InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // Check s3Instance existence - _, found := r.S3ClientCache.Get(s3InstanceResource.Name) + _, found := r.S3ClientCache.Get(s3InstanceResource.Namespace + "/" + s3InstanceResource.Name) // If the s3Instance does not exist, it is created based on the CR if !found { logger.Info("this S3Instance doesn't exist and will be created") @@ -127,37 +114,51 @@ func (r *S3InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) func (r *S3InstanceReconciler) handleS3InstanceUpdate(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (reconcile.Result, error) { logger := log.FromContext(ctx) - s3Client, found := r.S3ClientCache.Get(s3InstanceResource.Name) + s3ClientName := s3InstanceResource.Namespace + "/" + s3InstanceResource.Name + s3Client, found := r.S3ClientCache.Get(s3ClientName) if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache", s3InstanceResource.Name)} + err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache", s3ClientName)} logger.Error(err, "No client was found") } s3Config := s3Client.GetConfig() // Get S3_ACCESS_KEY and S3_SECRET_KEY related to this s3Instance - s3InstanceSecretSecretExpected, err := r.getS3InstanceSecret(ctx, s3InstanceResource) + s3InstanceSecretSecretExpected, err := r.getS3InstanceAccessSecret(ctx, s3InstanceResource) if err != nil { logger.Error(err, "Could not get s3InstanceSecret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretName) return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceUpdateFailed", - fmt.Sprintf("Updating secret of S3Instance %s has failed", s3InstanceResource.Name), err) + fmt.Sprintf("Updating secret of S3Instance %s has failed", s3ClientName), err) + } + + s3InstanceCaCertSecretExpected, err := r.getS3InstanceCaCertSecret(ctx, s3InstanceResource) + if err != nil { + logger.Error(err, "Could not get s3InstanceSecret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretName) + return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceCreationFailed", + fmt.Sprintf("Getting secret of S3Instance %s has failed", s3ClientName), err) + + } + + allowedNamepaces := []string{s3InstanceResource.Namespace} + if s3InstanceResource.Spec.AllowedNamespaces != nil { + allowedNamepaces = s3InstanceResource.Spec.AllowedNamespaces } // if s3Provider have change recreate totaly One Differ instance will be deleted and recreated - if s3Config.S3Provider != s3InstanceResource.Spec.S3Provider || s3Config.S3UrlEndpoint != s3InstanceResource.Spec.UrlEndpoint || s3Config.UseSsl != s3InstanceResource.Spec.UseSSL || s3Config.Region != s3InstanceResource.Spec.Region || !reflect.DeepEqual(s3Config.CaCertificatesBase64, s3InstanceResource.Spec.CaCertificatesBase64) || s3Config.AccessKey != string(s3InstanceSecretSecretExpected.Data["S3_ACCESS_KEY"]) || s3Config.SecretKey != string(s3InstanceSecretSecretExpected.Data["S3_SECRET_KEY"]) { + if s3Config.S3Provider != s3InstanceResource.Spec.S3Provider || s3Config.S3UrlEndpoint != s3InstanceResource.Spec.UrlEndpoint || s3Config.UseSsl != s3InstanceResource.Spec.UseSSL || s3Config.Region != s3InstanceResource.Spec.Region || !reflect.DeepEqual(s3Config.AllowedNamespaces, allowedNamepaces) || !reflect.DeepEqual(s3Config.CaCertificatesBase64, []string{string(s3InstanceCaCertSecretExpected.Data["ca.crt"])}) || s3Config.AccessKey != string(s3InstanceSecretSecretExpected.Data["S3_ACCESS_KEY"]) || s3Config.SecretKey != string(s3InstanceSecretSecretExpected.Data["S3_SECRET_KEY"]) { logger.Info("Instance in cache not equal to expected , cache will be prune and instance recreate", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretName) - r.S3ClientCache.Remove(s3InstanceResource.Name) + r.S3ClientCache.Remove(s3ClientName) return r.handleS3InstanceCreation(ctx, s3InstanceResource) } return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorSucceeded", metav1.ConditionTrue, "S3InstanceUpdated", - fmt.Sprintf("The S3Instance %s was updated was reconcile successfully", s3InstanceResource.Name), nil) + fmt.Sprintf("The S3Instance %s was updated was reconcile successfully", s3ClientName), nil) } func (r *S3InstanceReconciler) handleS3InstanceCreation(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (reconcile.Result, error) { logger := log.FromContext(ctx) - s3InstanceSecretSecret, err := r.getS3InstanceSecret(ctx, s3InstanceResource) + s3InstanceSecretSecret, err := r.getS3InstanceAccessSecret(ctx, s3InstanceResource) if err != nil { logger.Error(err, "Could not get s3InstanceSecret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretName) return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceCreationFailed", @@ -165,15 +166,27 @@ func (r *S3InstanceReconciler) handleS3InstanceCreation(ctx context.Context, s3I } - s3Config := &s3Factory.S3Config{S3Provider: s3InstanceResource.Spec.S3Provider, AccessKey: string(s3InstanceSecretSecret.Data["S3_ACCESS_KEY"]), SecretKey: string(s3InstanceSecretSecret.Data["S3_SECRET_KEY"]), S3UrlEndpoint: s3InstanceResource.Spec.UrlEndpoint, Region: s3InstanceResource.Spec.Region, UseSsl: s3InstanceResource.Spec.UseSSL, CaCertificatesBase64: s3InstanceResource.Spec.CaCertificatesBase64} + s3InstanceCaCertSecret, err := r.getS3InstanceCaCertSecret(ctx, s3InstanceResource) + if err != nil { + logger.Error(err, "Could not get S3InstanceCaCertSecret in namespace", "S3InstanceCaCertSecret", s3InstanceResource.Spec.CaCertSecretRef) + return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceCreationFailed", + fmt.Sprintf("Getting secret S3InstanceCaCertSecret %s has failed", s3InstanceResource.Name), err) + } + allowedNamepaces := []string{s3InstanceResource.Namespace} + if s3InstanceResource.Spec.AllowedNamespaces != nil { + allowedNamepaces = s3InstanceResource.Spec.AllowedNamespaces + } + + s3Config := &s3Factory.S3Config{S3Provider: s3InstanceResource.Spec.S3Provider, AccessKey: string(s3InstanceSecretSecret.Data["S3_ACCESS_KEY"]), SecretKey: string(s3InstanceSecretSecret.Data["S3_SECRET_KEY"]), S3UrlEndpoint: s3InstanceResource.Spec.UrlEndpoint, Region: s3InstanceResource.Spec.Region, UseSsl: s3InstanceResource.Spec.UseSSL, AllowedNamespaces: allowedNamepaces, CaCertificatesBase64: []string{string(s3InstanceCaCertSecret.Data["ca.crt"])}} + s3ClientName := s3InstanceResource.Namespace + "/" + s3InstanceResource.Name s3Client, err := s3Factory.GenerateS3Client(s3Config.S3Provider, s3Config) if err != nil { return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceCreationFailed", fmt.Sprintf("Error while creating s3Instance %s", s3InstanceResource.Name), err) } - r.S3ClientCache.Set(s3InstanceResource.Name, s3Client) + r.S3ClientCache.Set(s3ClientName, s3Client) return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorSucceeded", metav1.ConditionTrue, "S3InstanceCreated", fmt.Sprintf("The S3Instance %s was created successfully", s3InstanceResource.Name), nil) @@ -212,32 +225,12 @@ func (r *S3InstanceReconciler) SetupWithManager(mgr ctrl.Manager) error { // filterLogger := ctrl.Log.WithName("filterEvt") return ctrl.NewControllerManagedBy(mgr). For(&s3v1alpha1.S3Instance{}). - // The "secret owning" implies the reconcile loop will be called whenever a Secret owned - // by a S3Instance is created/updated/deleted. In other words, even when creating a single S3Instance, - // there is going to be several iterations. - Owns(&corev1.Secret{}). // See : https://sdk.operatorframework.io/docs/building-operators/golang/references/event-filtering/ WithEventFilter(predicate.Funcs{ - // Ignore updates to CR status in which case metadata.Generation does not change, // unless it is a change to the underlying Secret UpdateFunc: func(e event.UpdateEvent) bool { - - // To check if the update event is tied to a change on secret, - // we try to cast e.ObjectNew to a secret (only if it's not a S3Instance, which - // should prevent any TypeAssertionError based panic). - secretUpdate := false - newUser, _ := e.ObjectNew.(*s3v1alpha1.S3Instance) - if newUser == nil { - secretUpdate = (e.ObjectNew.(*corev1.Secret) != nil) - } - - return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() || secretUpdate - }, - // Ignore create events caused by the underlying secret's creation - CreateFunc: func(e event.CreateEvent) bool { - s3Instance, _ := e.Object.(*s3v1alpha1.S3Instance) - return s3Instance != nil + return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() }, DeleteFunc: func(e event.DeleteEvent) bool { // Evaluates to false if the object has been confirmed deleted. @@ -274,22 +267,24 @@ func (r *S3InstanceReconciler) setS3InstanceStatusConditionAndUpdate(ctx context func (r *S3InstanceReconciler) finalizeS3Instance(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) error { logger := log.FromContext(ctx) // Create S3Client - logger.Info(fmt.Sprintf("Search S3Instance %s to delete in cache , search instance in cache", s3InstanceResource.Name)) - _, found := r.S3ClientCache.Get(s3InstanceResource.Name) + s3ClientName := s3InstanceResource.Namespace + "/" + s3InstanceResource.Name + logger.Info(fmt.Sprintf("Search S3Instance %s to delete in cache , search instance in cache", s3ClientName)) + _, found := r.S3ClientCache.Get(s3ClientName) if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache cannot finalize", s3InstanceResource.Name)} + err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache cannot finalize", s3ClientName)} logger.Error(err, "No client was found") return err } - r.S3ClientCache.Remove(s3InstanceResource.Name) + r.S3ClientCache.Remove(s3ClientName) return nil } -func (r *S3InstanceReconciler) getS3InstanceSecret(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (corev1.Secret, error) { +func (r *S3InstanceReconciler) getS3InstanceAccessSecret(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (corev1.Secret, error) { logger := log.FromContext(ctx) secretsList := &corev1.SecretList{} s3InstanceSecret := corev1.Secret{} + secretFound := false err := r.List(ctx, secretsList, client.InNamespace(s3InstanceResource.Namespace)) if err != nil { @@ -299,7 +294,7 @@ func (r *S3InstanceReconciler) getS3InstanceSecret(ctx context.Context, s3Instan if len(secretsList.Items) == 0 { logger.Info("The s3instance's namespace doesn't appear to contain any secret") - return s3InstanceSecret, nil + return s3InstanceSecret, fmt.Errorf("no secret found in namespace") } // In all the secrets inside the s3instance's namespace, one should have a name equal to // the S3InstanceSecretRefName field. @@ -309,9 +304,55 @@ func (r *S3InstanceReconciler) getS3InstanceSecret(ctx context.Context, s3Instan for _, secret := range secretsList.Items { if secret.Name == s3InstanceSecretName { s3InstanceSecret = secret + secretFound = true + break + } + } + if secretFound { + return s3InstanceSecret, nil + } else { + return s3InstanceSecret, fmt.Errorf("secret not found in namespace") + } +} + +func (r *S3InstanceReconciler) getS3InstanceCaCertSecret(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (corev1.Secret, error) { + logger := log.FromContext(ctx) + + secretsList := &corev1.SecretList{} + s3InstanceCaCertSecret := corev1.Secret{} + secretFound := false + + if s3InstanceResource.Spec.CaCertSecretRef == "" { + return s3InstanceCaCertSecret, nil + } + + err := r.List(ctx, secretsList, client.InNamespace(s3InstanceResource.Namespace)) + if err != nil { + logger.Error(err, "An error occurred while listing the secrets in s3instance's namespace") + return s3InstanceCaCertSecret, fmt.Errorf("secretListingFailed") + } + + if len(secretsList.Items) == 0 { + logger.Info("The s3instance's namespace doesn't appear to contain any secret") + return s3InstanceCaCertSecret, nil + } + // In all the secrets inside the s3instance's namespace, one should have a name equal to + // the S3InstanceSecretRefName field. + s3InstanceCaCertSecretRef := s3InstanceResource.Spec.CaCertSecretRef + + // cmp.Or takes the first non "zero" value, see https://pkg.go.dev/cmp#Or + for _, secret := range secretsList.Items { + if secret.Name == s3InstanceCaCertSecretRef { + s3InstanceCaCertSecret = secret + secretFound = true break } } - return s3InstanceSecret, nil + if secretFound { + return s3InstanceCaCertSecret, nil + } else { + return s3InstanceCaCertSecret, fmt.Errorf("secret not found in namespace") + } + } diff --git a/controllers/user_controller.go b/controllers/user_controller.go index c5bd4d8..a302d3a 100644 --- a/controllers/user_controller.go +++ b/controllers/user_controller.go @@ -21,6 +21,7 @@ import ( "context" "fmt" "slices" + "strings" "time" corev1 "k8s.io/api/core/v1" @@ -83,19 +84,6 @@ func (r *S3UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } - // check if this object must be manage by this instance - if r.S3LabelSelectorValue != "" { - labelSelectorValue, found := userResource.Labels[utils.S3OperatorUserLabelSelectorKey] - if !found { - logger.Info("This user ressouce will not be manage by this instance because this instance require that Bucket get labelSelector and label selector not found", "req.Name", req.Name, "Bucket Labels", userResource.Labels, "S3OperatorBucketLabelSelectorKey", utils.S3OperatorBucketLabelSelectorKey) - return ctrl.Result{}, nil - } - if labelSelectorValue != r.S3LabelSelectorValue { - logger.Info("This user ressouce will not be manage by this instance because this instance require that Bucket get specific a specific labelSelector value", "req.Name", req.Name, "expected", r.S3LabelSelectorValue, "current", labelSelectorValue) - return ctrl.Result{}, nil - } - } - // Check if the userResource instance is marked to be deleted, which is // indicated by the deletion timestamp being set. The object will be deleted. if userResource.GetDeletionTimestamp() != nil { @@ -116,14 +104,18 @@ func (r *S3UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } } - // Check user existence on the S3 server - // Create S3Client s3Client, err := r.getS3InstanceForObject(ctx, userResource) if err != nil { - logger.Error(err, "an error occurred while getting s3Client") - return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", - "Getting s3Client in cache has failed", err) + if customErr, ok := err.(*s3ClientCache.S3ClientNotFound); ok { + logger.Error(err, "an error occurred while getting s3Client") + return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + customErr.Reason, err) + } else { + logger.Error(err, "an error occurred while getting s3Client") + return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + "Unknown error occured while getting s3Client", err) + } } found, err := s3Client.UserExist(userResource.Spec.AccessKey) @@ -145,14 +137,20 @@ func (r *S3UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResource *s3v1alpha1.S3User) (reconcile.Result, error) { logger := log.FromContext(ctx) + // Create S3Client s3Client, err := r.getS3InstanceForObject(ctx, userResource) if err != nil { - logger.Error(err, "an error occurred while getting s3Client") - return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", - "Getting s3Client in cache has failed", err) + if customErr, ok := err.(*s3ClientCache.S3ClientNotFound); ok { + logger.Error(err, "an error occurred while getting s3Client") + return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + customErr.Reason, err) + } else { + logger.Error(err, "an error occurred while getting s3Client") + return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + "Unknown error occured while getting s3Client", err) + } } - // --- Begin Secret management section userOwnedSecret, err := r.getUserSecret(ctx, userResource) if err != nil { @@ -270,13 +268,21 @@ func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResourc func (r *S3UserReconciler) handleS3NewUser(ctx context.Context, userResource *s3v1alpha1.S3User) (reconcile.Result, error) { logger := log.FromContext(ctx) + // Create S3Client s3Client, err := r.getS3InstanceForObject(ctx, userResource) if err != nil { - logger.Error(err, "an error occurred while getting s3Client") - return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", - "Getting s3Client in cache has failed", err) + if customErr, ok := err.(*s3ClientCache.S3ClientNotFound); ok { + logger.Error(err, "an error occurred while getting s3Client") + return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + customErr.Reason, err) + } else { + logger.Error(err, "an error occurred while getting s3Client") + return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "FailedS3Client", + "Unknown error occured while getting s3Client", err) + } } + // Generating a random secret key secretKey, err := password.Generate(20, true, false, true) if err != nil { @@ -397,7 +403,6 @@ func (r *S3UserReconciler) addPoliciesToUser(ctx context.Context, userResource * // Create S3Client s3Client, err := r.getS3InstanceForObject(ctx, userResource) if err != nil { - logger.Error(err, "an error occurred while getting s3Client") return err } policies := userResource.Spec.Policies @@ -613,19 +618,37 @@ func (r *S3UserReconciler) getS3InstanceForObject(ctx context.Context, userResou logger.Info("Bucket resource doesn't refer to s3Instance, failback to default one") s3Client, found := r.S3ClientCache.Get("default") if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: "No default client was found"} - logger.Error(err, "No default client was found") + err := &s3ClientCache.S3ClientNotFound{Reason: "Client not found"} + logger.Error(err, "Client \"default\" was not found") return nil, err + } else { + if utils.IsAllowedNamespaces(userResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientNotFound{Reason: "Client \"default\" was not found"} + return nil, err + } } - return s3Client, nil } else { - logger.Info(fmt.Sprintf("Bucket resource doesn't refer to s3Instance: %s, search instance in cache", userResource.Spec.S3InstanceRef)) - s3Client, found := r.S3ClientCache.Get(userResource.Spec.S3InstanceRef) + logger.Info(fmt.Sprintf("User resource doesn't refer to s3Instance: %s, search instance in cache", userResource.Spec.S3InstanceRef)) + clientName := "" + if strings.Contains(userResource.Spec.S3InstanceRef, "/") { + clientName = userResource.Spec.S3InstanceRef + } else { + clientName = userResource.Namespace + "/" + userResource.Spec.S3InstanceRef + } + s3Client, found := r.S3ClientCache.Get(clientName) if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache", userResource.Spec.S3InstanceRef)} - logger.Error(err, "No client was found") + err := &s3ClientCache.S3ClientNotFound{Reason: fmt.Sprintf("S3InstanceRef: %s not found in cache", clientName)} + logger.Error(err, fmt.Sprintf("S3InstanceRef: %s not found in cache", clientName)) + return nil, err + } + logger.Info("Check if userRessource %s can use S3Instance %s", userResource.Name, clientName) + if utils.IsAllowedNamespaces(userResource.Namespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &s3ClientCache.S3ClientNotFound{Reason: fmt.Sprintf("Client %s is not allowed in this namespace", userResource.Spec.S3InstanceRef)} return nil, err } - return s3Client, nil } } diff --git a/go.mod b/go.mod index e781b36..85e66b8 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.4 github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect @@ -29,6 +30,7 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/gobwas/glob v0.2.3 github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect diff --git a/go.sum b/go.sum index 9790a39..adaf635 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= @@ -37,6 +39,8 @@ github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= diff --git a/internal/s3/factory/interface.go b/internal/s3/factory/interface.go index dbd14b2..5af0405 100644 --- a/internal/s3/factory/interface.go +++ b/internal/s3/factory/interface.go @@ -48,6 +48,7 @@ type S3Config struct { UseSsl bool CaCertificatesBase64 []string CaBundlePath string + AllowedNamespaces []string } func GenerateS3Client(s3Provider string, S3Config *S3Config) (S3Client, error) { @@ -60,7 +61,7 @@ func GenerateS3Client(s3Provider string, S3Config *S3Config) (S3Client, error) { return nil, fmt.Errorf("s3 provider " + s3Provider + "not supported") } -func GenerateDefaultS3Client(s3Provider string, s3UrlEndpoint string, accessKey string, secretKey string, region string, useSsl bool, caCertificatesBase64 []string, caBundlePath string) (S3Client, error) { +func GenerateDefaultS3Client(s3Provider string, s3UrlEndpoint string, accessKey string, secretKey string, region string, useSsl bool, caCertificatesBase64 []string, caBundlePath string, allowedNamespaces []string) (S3Client, error) { // For S3 access key and secret key, we first try to read the values from environment variables. // Only if these are not defined do we use the respective flags. @@ -82,7 +83,7 @@ func GenerateDefaultS3Client(s3Provider string, s3UrlEndpoint string, accessKey return newMockedS3Client(), nil } if s3Provider == "minio" { - S3Config := &S3Config{S3Provider: s3Provider, S3UrlEndpoint: s3UrlEndpoint, Region: region, AccessKey: accessKeyFromEnvIfAvailable, SecretKey: secretKeyFromEnvIfAvailable, UseSsl: useSsl, CaCertificatesBase64: caCertificatesBase64, CaBundlePath: caBundlePath} + S3Config := &S3Config{S3Provider: s3Provider, S3UrlEndpoint: s3UrlEndpoint, Region: region, AccessKey: accessKeyFromEnvIfAvailable, SecretKey: secretKeyFromEnvIfAvailable, UseSsl: useSsl, CaCertificatesBase64: caCertificatesBase64, CaBundlePath: caBundlePath, AllowedNamespaces: allowedNamespaces} return newMinioS3Client(S3Config), nil } return nil, fmt.Errorf("s3 provider " + s3Provider + "not supported") diff --git a/internal/s3/factory/minioS3Client.go b/internal/s3/factory/minioS3Client.go index d1e51ee..2692b32 100644 --- a/internal/s3/factory/minioS3Client.go +++ b/internal/s3/factory/minioS3Client.go @@ -63,6 +63,7 @@ func newMinioS3Client(S3Config *S3Config) *MinioS3Client { } func addTransportOptions(S3Config *S3Config, minioOptions *minio.Options) { + if len(S3Config.CaCertificatesBase64) > 0 { rootCAs, _ := x509.SystemCertPool() diff --git a/internal/s3/s3ClientCache.go b/internal/s3/s3ClientCache.go index e8b5851..dabc40d 100644 --- a/internal/s3/s3ClientCache.go +++ b/internal/s3/s3ClientCache.go @@ -4,9 +4,15 @@ import ( "fmt" "sync" + ctrl "sigs.k8s.io/controller-runtime" + "github.com/InseeFrLab/s3-operator/internal/s3/factory" ) +var ( + logger = ctrl.Log.WithValues("logger", "s3clientCache") +) + // Cache is a basic in-memory key-value cache implementation. type S3ClientCache struct { items map[string]factory.S3Client // The map storing key-value pairs. @@ -15,6 +21,7 @@ type S3ClientCache struct { // New creates a new Cache instance. func New() *S3ClientCache { + logger.Info("Creation of S3ClientCache successfully") return &S3ClientCache{ items: make(map[string]factory.S3Client), } @@ -24,7 +31,7 @@ func New() *S3ClientCache { func (c *S3ClientCache) Set(key string, value factory.S3Client) { c.mu.Lock() defer c.mu.Unlock() - + logger.Info(fmt.Sprintf("Add S3Client %s in cache successfully", key)) c.items[key] = value } @@ -33,6 +40,7 @@ func (c *S3ClientCache) Set(key string, value factory.S3Client) { func (c *S3ClientCache) Get(key string) (factory.S3Client, bool) { c.mu.Lock() defer c.mu.Unlock() + logger.Info(fmt.Sprintf("Try getting S3Client %s in cache", key)) value, found := c.items[key] return value, found @@ -42,6 +50,7 @@ func (c *S3ClientCache) Get(key string) (factory.S3Client, bool) { func (c *S3ClientCache) Remove(key string) { c.mu.Lock() defer c.mu.Unlock() + logger.Info(fmt.Sprintf("Successfully remove S3Client %s in cache", key)) delete(c.items, key) } @@ -61,10 +70,31 @@ func (c *S3ClientCache) Pop(key string) (factory.S3Client, bool) { return value, found } +func (c *S3ClientCache) GetAllowedNamespaces(key string) []string { + c.mu.Lock() + defer c.mu.Unlock() + var allowedNamepaces []string + + logger.Info(fmt.Sprintf("Get AllowedNamespaces for S3Client %s in cache", key)) + + for _, s3Client := range c.items { + allowedNamepaces = append(allowedNamepaces, s3Client.GetConfig().AllowedNamespaces...) + } + return allowedNamepaces +} + type S3ClientCacheError struct { Reason string } +type S3ClientNotFound struct { + Reason string +} + func (r *S3ClientCacheError) Error() string { return fmt.Sprintf("%s", r.Reason) } + +func (r *S3ClientNotFound) Error() string { + return fmt.Sprintf("%s", r.Reason) +} diff --git a/internal/utils/glob/glob.go b/internal/utils/glob/glob.go new file mode 100644 index 0000000..f4f67d5 --- /dev/null +++ b/internal/utils/glob/glob.go @@ -0,0 +1,41 @@ +package glob + +import ( + "strings" + + "github.com/InseeFrLab/s3-operator/internal/utils/regex" + "github.com/gobwas/glob" +) + +const ( + EXACT = "exact" + GLOB = "glob" + REGEXP = "regexp" +) + +func Match(pattern, text string, separators ...rune) bool { + compiledGlob, err := glob.Compile(pattern, separators...) + if err != nil { + return false + } + return compiledGlob.Match(text) +} + +// MatchStringInList will return true if item is contained in list. +// patternMatch; can be set to exact, glob, regexp. +// If patternMatch; is set to exact, the item must be an exact match. +// If patternMatch; is set to glob, the item must match a glob pattern. +// If patternMatch; is set to regexp, the item must match a regular expression or glob. +func MatchStringInList(list []string, item string, patternMatch string) bool { + for _, ll := range list { + // If string is wrapped in "/", assume it is a regular expression. + if patternMatch == REGEXP && strings.HasPrefix(ll, "/") && strings.HasSuffix(ll, "/") && regex.Match(ll[1:len(ll)-1], item) { + return true + } else if (patternMatch == REGEXP || patternMatch == GLOB) && Match(ll, item) { + return true + } else if patternMatch == EXACT && item == ll { + return true + } + } + return false +} diff --git a/internal/utils/regex/regex.go b/internal/utils/regex/regex.go new file mode 100644 index 0000000..cda0051 --- /dev/null +++ b/internal/utils/regex/regex.go @@ -0,0 +1,17 @@ +package regex + +import ( + "github.com/dlclark/regexp2" +) + +func Match(pattern, text string) bool { + compiledRegex, err := regexp2.Compile(pattern, 0) + if err != nil { + return false + } + regexMatch, err := compiledRegex.MatchString(text) + if err != nil { + return false + } + return regexMatch +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 4f69b74..648573d 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -3,6 +3,7 @@ package utils import ( "time" + glob "github.com/InseeFrLab/s3-operator/internal/utils/glob" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -27,3 +28,7 @@ func UpdateConditions(existingConditions []metav1.Condition, newCondition metav1 return append([]metav1.Condition{newCondition}, existingConditions...) } + +func IsAllowedNamespaces(namespace string, namespaces []string) bool { + return glob.MatchStringInList(namespaces, namespace, glob.REGEXP) +} diff --git a/main.go b/main.go index e48edab..cd06569 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "flag" "fmt" "os" + "strings" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -29,7 +30,6 @@ import ( controllers "github.com/InseeFrLab/s3-operator/controllers" s3ClientCache "github.com/InseeFrLab/s3-operator/internal/s3" "github.com/InseeFrLab/s3-operator/internal/s3/factory" - "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -85,6 +85,7 @@ func main() { var pathDeletion bool var s3userDeletion bool var s3LabelSelector string + var allowedNamespaces string //K8S related variable var overrideExistingSecret bool @@ -96,20 +97,20 @@ func main() { "Enabling this will ensure there is only one active controller manager.") // S3 related flags - flag.StringVar(&s3Provider, "s3-provider", "minio", "S3 provider (possible values : minio, mockedS3Provider)") - flag.StringVar(&s3EndpointUrl, "s3-endpoint-url", "localhost:9000", "Hostname (or hostname:port) of the S3 server") - flag.StringVar(&accessKey, "s3-access-key", "ROOTNAME", "The accessKey of the acount") - flag.StringVar(&secretKey, "s3-secret-key", "CHANGEME123", "The secretKey of the acount") - flag.StringVar(&s3LabelSelector, "s3-label-selector", "", "label selector to filter object managed by this operator if empty all objects are managed") + flag.StringVar(&s3Provider, "s3-provider", "", "S3 provider (possible values : minio, mockedS3Provider)") + flag.StringVar(&s3EndpointUrl, "s3-endpoint-url", "", "Hostname (or hostname:port) of the S3 server") + flag.StringVar(&accessKey, "s3-access-key", "", "The accessKey of the acount") + flag.StringVar(&secretKey, "s3-secret-key", "", "The secretKey of the acount") flag.Var(&caCertificatesBase64, "s3-ca-certificate-base64", "(Optional) Base64 encoded, PEM format certificate file for a certificate authority, for https requests to S3") flag.StringVar(&caCertificatesBundlePath, "s3-ca-certificate-bundle-path", "", "(Optional) Path to a CA certificate file, for https requests to S3") - flag.StringVar(®ion, "region", "us-east-1", "The region to configure for the S3 client") + flag.StringVar(®ion, "region", "", "The region to configure for the S3 client") flag.BoolVar(&useSsl, "useSsl", true, "Use of SSL/TLS to connect to the S3 endpoint") flag.BoolVar(&bucketDeletion, "bucket-deletion", false, "Trigger bucket deletion on the S3 backend upon CR deletion. Will fail if bucket is not empty.") flag.BoolVar(&policyDeletion, "policy-deletion", false, "Trigger policy deletion on the S3 backend upon CR deletion") flag.BoolVar(&pathDeletion, "path-deletion", false, "Trigger path deletion on the S3 backend upon CR deletion. Limited to deleting the `.keep` files used by the operator.") flag.BoolVar(&s3userDeletion, "s3user-deletion", false, "Trigger S3 deletion on the S3 backend upon CR deletion") flag.BoolVar(&overrideExistingSecret, "override-existing-secret", false, "Override existing secret associated to user in case of the secret already exist") + flag.StringVar(&allowedNamespaces, "allowed-namespaces", "", "namespace that are allowed to use default s3instance") opts := zap.Options{ Development: true, @@ -155,14 +156,10 @@ func main() { } s3ClientCache := s3ClientCache.New() - // Creation of the default S3 client - s3DefaultClient, err := factory.GenerateDefaultS3Client(s3Provider, s3EndpointUrl, accessKey, secretKey, region, useSsl, caCertificatesBase64, caCertificatesBundlePath) + s3DefaultClient, err := factory.GenerateDefaultS3Client(s3Provider, s3EndpointUrl, accessKey, secretKey, region, useSsl, caCertificatesBase64, caCertificatesBundlePath, strings.Split(allowedNamespaces, ",")) if err != nil { - // setupLog.Log.Error(err, err.Error()) - // fmt.Print(s3Client) - // fmt.Print(err) setupLog.Error(err, "an error occurred while creating the S3 client", "s3Client", s3DefaultClient) os.Exit(1) } @@ -181,37 +178,37 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Bucket") os.Exit(1) } - // if err = (&controllers.PathReconciler{ - // Client: mgr.GetClient(), - // Scheme: mgr.GetScheme(), - // S3ClientCache: s3ClientCache, - // PathDeletion: pathDeletion, - // S3LabelSelectorValue: s3LabelSelector, - // }).SetupWithManager(mgr); err != nil { - // setupLog.Error(err, "unable to create controller", "controller", "Path") - // os.Exit(1) - // } - // if err = (&controllers.PolicyReconciler{ - // Client: mgr.GetClient(), - // Scheme: mgr.GetScheme(), - // S3ClientCache: s3ClientCache, - // PolicyDeletion: policyDeletion, - // S3LabelSelectorValue: s3LabelSelector, - // }).SetupWithManager(mgr); err != nil { - // setupLog.Error(err, "unable to create controller", "controller", "Policy") - // os.Exit(1) - // } - // if err = (&controllers.S3UserReconciler{ - // Client: mgr.GetClient(), - // Scheme: mgr.GetScheme(), - // S3ClientCache: s3ClientCache, - // S3UserDeletion: s3userDeletion, - // OverrideExistingSecret: overrideExistingSecret, - // S3LabelSelectorValue: s3LabelSelector, - // }).SetupWithManager(mgr); err != nil { - // setupLog.Error(err, "unable to create controller", "controller", "S3User") - // os.Exit(1) - // } + if err = (&controllers.PathReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + S3ClientCache: s3ClientCache, + PathDeletion: pathDeletion, + S3LabelSelectorValue: s3LabelSelector, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Path") + os.Exit(1) + } + if err = (&controllers.PolicyReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + S3ClientCache: s3ClientCache, + PolicyDeletion: policyDeletion, + S3LabelSelectorValue: s3LabelSelector, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Policy") + os.Exit(1) + } + if err = (&controllers.S3UserReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + S3ClientCache: s3ClientCache, + S3UserDeletion: s3userDeletion, + OverrideExistingSecret: overrideExistingSecret, + S3LabelSelectorValue: s3LabelSelector, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "S3User") + os.Exit(1) + } if err = (&controllers.S3InstanceReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(),