diff --git a/README.md b/README.md index 7873585..e2cc1ae 100644 --- a/README.md +++ b/README.md @@ -74,25 +74,16 @@ The operator exposes a few parameters, meant to be set as arguments, though it's The parameters are summarized in the table below : -| Flag name | Default | Environment variable | Multiple values allowed | Description | -| ------------------------------- | ---------------- | -------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `health-probe-bind-address` | `:8081` | - | no | The address the probe endpoint binds to. Comes from Operator SDK. | -| `leader-elect` | `false` | - | no | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. Comes from Operator SDK. | -| `metrics-bind-address` | `:8080` | - | no | The address the metric endpoint binds to. Comes from Operator SDK. | -| `region` | `us-east-1` | - | no | The region to configure for the S3 client. | -| `s3-access-key` | - | `S3_ACCESS_KEY` | no | The access key used to interact with the S3 server. | -| `s3-ca-certificate-base64` | - | - | yes | (Optional) Base64 encoded, PEM format CA certificate, for https requests to the S3 server. | -| `s3-ca-certificate-bundle-path` | - | - | no | (Optional) Path to a CA certificates bundle file, for https requests to the S3 server. | -| `s3-endpoint-url` | `localhost:9000` | - | no | Hostname (or hostname:port) of the S3 server. | -| `s3-provider` | `minio` | - | no | S3 provider (possible values : `minio`, `mockedS3Provider`) | -| `s3-secret-key` | - | `S3_SECRET_KEY` | no | The secret key used to interact with the S3 server. | -| `useSsl` | true | - | no | Use of SSL/TLS to connect to the S3 server | -| `bucket-deletion` | false | - | no | Trigger bucket deletion on the S3 backend upon CR deletion. Will fail if bucket is not empty. | -| `policy-deletion` | false | - | no | Trigger policy deletion on the S3 backend upon CR deletion | -| `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 | +| Flag name | Default | Environment variable | Multiple values allowed | Description | +| --------------------------- | ------- | -------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `health-probe-bind-address` | `:8081` | - | no | The address the probe endpoint binds to. Comes from Operator SDK. | +| `leader-elect` | `false` | - | no | Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. Comes from Operator SDK. | +| `metrics-bind-address` | `:8080` | - | no | The address the metric endpoint binds to. Comes from Operator SDK. | | +| `bucket-deletion` | false | - | no | Trigger bucket deletion on the S3 backend upon CR deletion. Will fail if bucket is not empty. | +| `policy-deletion` | false | - | no | Trigger policy deletion on the S3 backend upon CR deletion | +| `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 | ## Minimal rights needed to work The Operator need at least this rights: @@ -170,6 +161,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: [] # namespaces allowed to have buckets, policies, ... Wildcard prefix/suffix allowed. If empty only the same namespace as s3instance is allowed ``` ### Bucket example @@ -307,6 +299,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..913e971 100644 --- a/api/v1alpha1/s3instance_types.go +++ b/api/v1alpha1/s3instance_types.go @@ -28,27 +28,48 @@ type S3InstanceSpec struct { // type of the S3Instance // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum S3Provider string `json:"s3Provider"` // url of the S3Instance // +kubebuilder:validation:Required - UrlEndpoint string `json:"urlEndpoint"` + Url string `json:"url"` - // SecretName associated to the S3Instance containing accessKey and secretKey + // Ref to Secret associated to the S3Instance containing accessKey and secretKey // +kubebuilder:validation:Required - SecretName string `json:"secretName"` + SecretRef string `json:"secretRef"` // region associated to the S3Instance - // +kubebuilder:validation:Required + // +kubebuilder:validation:Optional Region string `json:"region"` - // useSSL when connecting to the S3Instance + // Secret containing key ca.crt with the certificate associated to the S3InstanceUrl + // +kubebuilder:validation:Optional + 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"` + + // BucketDeletionEnabled Trigger bucket deletion on the S3 backend upon CR deletion. Will fail if bucket is not empty. + // +kubebuilder:validation:Optional + // +kubebuilder:default=false + BucketDeletionEnabled bool `json:"bucketDeletionEnabled,omitempty"` + + // PolicyDeletionEnabled Trigger policy deletion on the S3 backend upon CR deletion. + // +kubebuilder:validation:Optional + // +kubebuilder:default=false + PolicyDeletionEnabled bool `json:"policyDeletionEnabled,omitempty"` + + // PathDeletionEnabled Trigger path deletion on the S3 backend upon CR deletion. Limited to deleting the `.keep` files used by the operator. // +kubebuilder:validation:Optional - UseSSL bool `json:"useSSL,omitempty"` + // +kubebuilder:default=false + PathDeletionEnabled bool `json:"pathDeletionEnabled,omitempty"` - // CaCertificatesBase64 associated to the S3InstanceUrl + // S3UserDeletionEnabled Trigger S3 deletion on the S3 backend upon CR deletion. // +kubebuilder:validation:Optional - CaCertificatesBase64 []string `json:"caCertificateBase64,omitempty"` + // +kubebuilder:default=false + S3UserDeletionEnabled bool `json:"s3UserDeletionEnabled,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/types.go b/api/v1alpha1/types.go new file mode 100644 index 0000000..40b8352 --- /dev/null +++ b/api/v1alpha1/types.go @@ -0,0 +1,16 @@ +package v1alpha1 + +// Definitions to manage status condition types +const ( + // ConditionReconciled represents the status of the resource reconciliation + ConditionReconciled = "Reconciled" +) + +// Definitions to manage status condition reasons +const ( + Reconciling = "Reconciling" + Unreachable = "Unreachable" + CreationFailure = "CreationFailure" + Reconciled = "Reconciled" + DeletionFailure = "DeletionFailure" +) 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..d94d573 100644 --- a/config/crd/bases/s3.onyxia.sh_s3instances.yaml +++ b/config/crd/bases/s3.onyxia.sh_s3instances.yaml @@ -35,32 +35,33 @@ 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 s3Provider: description: type of the S3Instance type: string - secretName: - description: SecretName associated to the S3Instance containing accessKey - and secretKey + secretRef: + description: Ref to Secret associated to the S3Instance containing + accessKey and secretKey type: string - urlEndpoint: + url: description: url of the S3Instance type: string - useSSL: - description: useSSL when connecting to the S3Instance - type: boolean required: - - region - s3Provider - - secretName - - urlEndpoint + - secretRef + - url type: object status: description: S3InstanceStatus defines the observed state of S3Instance 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/config/samples/s3.onyxia.sh_v1alpha1_s3instance.yaml b/config/samples/s3.onyxia.sh_v1alpha1_s3instance.yaml index 198da17..6c1f72f 100644 --- a/config/samples/s3.onyxia.sh_v1alpha1_s3instance.yaml +++ b/config/samples/s3.onyxia.sh_v1alpha1_s3instance.yaml @@ -2,15 +2,32 @@ apiVersion: s3.onyxia.sh/v1alpha1 kind: S3Instance metadata: labels: - app.kubernetes.io/name: bucket - app.kubernetes.io/instance: bucket-sample + app.kubernetes.io/name: s3instance + app.kubernetes.io/instance: s3instance-sample app.kubernetes.io/part-of: s3-operator app.kubernetes.io/managed-by: kustomize app.kubernetes.io/created-by: s3-operator - name: s3-default-instance + name: s3instance-sample spec: s3Provider: minio - urlEndpoint: minio.example.com - secretName: minio-credentials - region: us-east-1 - useSSL: true + url: https://minio.example.com + secretRef: minio-credentials + caCertSecretRef: minio-certificates + # allowedNamespaces: "*" # if not present only resources from the same namespace is allowed + # region: us-east-1 +--- +apiVersion: v1 +kind: Secret +metadata: + name: mysecret +type: Opaque +data: + password: +--- +apiVersion: v1 +kind: Secret +metadata: + name: mysecret +type: Opaque +data: + password: diff --git a/controllers/bucket_controller.go b/controllers/bucket_controller.go index 5e88562..130fde6 100644 --- a/controllers/bucket_controller.go +++ b/controllers/bucket_controller.go @@ -42,10 +42,8 @@ import ( // BucketReconciler reconciles a Bucket object type BucketReconciler struct { client.Client - Scheme *runtime.Scheme - S3ClientCache *s3ClientCache.S3ClientCache - BucketDeletion bool - S3LabelSelectorValue string + Scheme *runtime.Scheme + S3ClientCache *s3ClientCache.S3ClientCache } //+kubebuilder:rbac:groups=s3.onyxia.sh,resources=buckets,verbs=get;list;watch;create;update;patch;delete @@ -74,19 +72,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 @@ -129,11 +114,17 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } // Create S3Client - s3Client, err := r.getS3InstanceForObject(ctx, bucketResource) + s3Client, err := r.getS3InstanceForObject(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 @@ -261,12 +252,12 @@ func (r *BucketReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *BucketReconciler) finalizeBucket(ctx context.Context, bucketResource *s3v1alpha1.Bucket) error { logger := log.FromContext(ctx) - s3Client, err := r.getS3InstanceForObject(ctx, bucketResource) + s3Client, err := r.getS3InstanceForObject(bucketResource) if err != nil { logger.Error(err, "an error occurred while getting s3Client") return err } - if r.BucketDeletion { + if s3Client.GetConfig().BucketDeletionEnabled { return s3Client.DeleteBucket(bucketResource.Spec.Name) } return nil @@ -295,26 +286,6 @@ func (r *BucketReconciler) SetBucketStatusConditionAndUpdate(ctx context.Context return ctrl.Result{}, srcError } -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") - 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") - 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) - if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s, not found in cache", bucketResource.Spec.S3InstanceRef)} - logger.Error(err, "No client was found") - return nil, err - } - return s3Client, nil - } +func (r *BucketReconciler) getS3InstanceForObject(bucketResource *s3v1alpha1.Bucket) (factory.S3Client, error) { + return r.S3ClientCache.GetS3Instance(bucketResource.Name, bucketResource.Namespace, bucketResource.Spec.S3InstanceRef) } diff --git a/controllers/path_controller.go b/controllers/path_controller.go index f05549a..ade078f 100644 --- a/controllers/path_controller.go +++ b/controllers/path_controller.go @@ -42,10 +42,8 @@ import ( // PathReconciler reconciles a Path object type PathReconciler struct { client.Client - Scheme *runtime.Scheme - S3ClientCache *s3ClientCache.S3ClientCache - PathDeletion bool - S3LabelSelectorValue string + Scheme *runtime.Scheme + S3ClientCache *s3ClientCache.S3ClientCache } //+kubebuilder:rbac:groups=s3.onyxia.sh,resources=paths,verbs=get;list;watch;create;update;patch;delete @@ -74,19 +72,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 @@ -130,11 +115,17 @@ func (r *PathReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. } // Create S3Client - s3Client, err := r.getS3InstanceForObject(ctx, pathResource) + s3Client, err := r.getS3InstanceForObject(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 @@ -208,13 +199,13 @@ func (r *PathReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *PathReconciler) finalizePath(ctx context.Context, pathResource *s3v1alpha1.Path) error { logger := log.FromContext(ctx) - s3Client, err := r.getS3InstanceForObject(ctx, pathResource) + s3Client, err := r.getS3InstanceForObject(pathResource) if err != nil { logger.Error(err, "an error occurred while getting s3Client") return err } - if r.PathDeletion { + if s3Client.GetConfig().PathDeletionEnabled { var failedPaths []string = make([]string, 0) for _, path := range pathResource.Spec.Paths { @@ -261,26 +252,6 @@ func (r *PathReconciler) SetPathStatusConditionAndUpdate(ctx context.Context, pa return ctrl.Result{}, srcError } -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") - 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") - 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) - if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache", pathResource.Spec.S3InstanceRef)} - logger.Error(err, "No client was found") - return nil, err - } - return s3Client, nil - } +func (r *PathReconciler) getS3InstanceForObject(pathResource *s3v1alpha1.Path) (factory.S3Client, error) { + return r.S3ClientCache.GetS3Instance(pathResource.Name, pathResource.Namespace, pathResource.Spec.S3InstanceRef) } diff --git a/controllers/policy_controller.go b/controllers/policy_controller.go index 37c3ee8..9319482 100644 --- a/controllers/policy_controller.go +++ b/controllers/policy_controller.go @@ -45,10 +45,8 @@ import ( // PolicyReconciler reconciles a Policy object type PolicyReconciler struct { client.Client - Scheme *runtime.Scheme - S3ClientCache *s3ClientCache.S3ClientCache - PolicyDeletion bool - S3LabelSelectorValue string + Scheme *runtime.Scheme + S3ClientCache *s3ClientCache.S3ClientCache } //+kubebuilder:rbac:groups=s3.onyxia.sh,resources=policies,verbs=get;list;watch;create;update;patch;delete @@ -77,19 +75,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 @@ -135,11 +120,17 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // Policy lifecycle management (other than deletion) starts here // Create S3Client - s3Client, err := r.getS3InstanceForObject(ctx, policyResource) + s3Client, err := r.getS3InstanceForObject(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 @@ -235,12 +226,12 @@ func IsPolicyMatchingWithCustomResource(policyResource *s3v1alpha1.Policy, effec func (r *PolicyReconciler) finalizePolicy(ctx context.Context, policyResource *s3v1alpha1.Policy) error { logger := log.FromContext(ctx) - s3Client, err := r.getS3InstanceForObject(ctx, policyResource) + s3Client, err := r.getS3InstanceForObject(policyResource) if err != nil { logger.Error(err, "an error occurred while getting s3Client") return err } - if r.PolicyDeletion { + if s3Client.GetConfig().PolicyDeletionEnabled { return s3Client.DeletePolicy(policyResource.Spec.Name) } return nil @@ -269,25 +260,6 @@ func (r *PolicyReconciler) SetPolicyStatusConditionAndUpdate(ctx context.Context return ctrl.Result{}, srcError } -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") - 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") - 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) - if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache", policyResource.Spec.S3InstanceRef)} - logger.Error(err, "No client was found") - return nil, err - } - return s3Client, nil - } +func (r *PolicyReconciler) getS3InstanceForObject(policyResource *s3v1alpha1.Policy) (factory.S3Client, error) { + return r.S3ClientCache.GetS3Instance(policyResource.Name, policyResource.Namespace, policyResource.Spec.S3InstanceRef) } diff --git a/controllers/s3instance_controller.go b/controllers/s3instance_controller.go index c46f1b5..93e50d0 100644 --- a/controllers/s3instance_controller.go +++ b/controllers/s3instance_controller.go @@ -19,14 +19,15 @@ package controllers import ( "context" "fmt" - "reflect" + "slices" "time" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" + k8sapierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -39,8 +40,6 @@ import ( s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" s3ClientCache "github.com/InseeFrLab/s3-operator/internal/s3" s3Factory "github.com/InseeFrLab/s3-operator/internal/s3/factory" - - utils "github.com/InseeFrLab/s3-operator/internal/utils" ) // S3InstanceReconciler reconciles a S3Instance object @@ -49,6 +48,7 @@ type S3InstanceReconciler struct { Scheme *runtime.Scheme S3ClientCache *s3ClientCache.S3ClientCache S3LabelSelectorValue string + ReconcilePeriod time.Duration } const ( @@ -71,137 +71,206 @@ func (r *S3InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) s3InstanceResource := &s3v1alpha1.S3Instance{} err := r.Get(ctx, req.NamespacedName, s3InstanceResource) if err != nil { - if errors.IsNotFound(err) { - logger.Info(fmt.Sprintf("The S3InstanceResource CR %s has been removed. NOOP", req.Name)) + if k8sapierrors.IsNotFound(err) { + logger.Info(fmt.Sprintf("The S3InstanceResource CR %s has been removed. NOOP", req.Name), "NamespacedName", req.NamespacedName.String()) return ctrl.Result{}, nil } - logger.Error(err, "An error occurred when fetching the S3InstanceResource from Kubernetes") + logger.Error(err, "Failed to get S3InstanceResource", "NamespacedName", req.NamespacedName.String()) 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 + // Let's just set the status as Unknown when no status are available + if len(s3InstanceResource.Status.Conditions) == 0 { + meta.SetStatusCondition(&s3InstanceResource.Status.Conditions, metav1.Condition{Type: s3v1alpha1.ConditionReconciled, Status: metav1.ConditionUnknown, ObservedGeneration: s3InstanceResource.Generation, Reason: s3v1alpha1.Reconciling, Message: "Starting reconciliation"}) + if err = r.Status().Update(ctx, s3InstanceResource); err != nil { + logger.Error(err, "Failed to update s3InstanceResource status", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, err } - 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 + + // Let's re-fetch the GrafanaInstance Custom Resource after update the status + // so that we have the latest state of the resource on the cluster and we will avoid + // raise the issue "the object has been modified, please apply + // your changes to the latest version and try again" which would re-trigger the reconciliation + // if we try to update it again in the following operations + if err := r.Get(ctx, req.NamespacedName, s3InstanceResource); err != nil { + logger.Error(err, "Failed to re-fetch GrafanaInstance", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, err + } + } + + // Add finalizer for this CR + if !controllerutil.ContainsFinalizer(s3InstanceResource, s3InstanceFinalizer) { + logger.Info("adding finalizer to s3Instance", "NamespacedName", req.NamespacedName.String()) + if ok := controllerutil.AddFinalizer(s3InstanceResource, s3InstanceFinalizer); !ok { + logger.Error(err, "Failed to add finalizer into the s3Instance", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{Requeue: true}, nil + } + + if err = r.Update(ctx, s3InstanceResource); err != nil { + logger.Error(err, "an error occurred when adding finalizer from s3Instance", "s3Instance", s3InstanceResource.Name) + return ctrl.Result{}, err + } + + // Let's re-fetch the S3Instance Custom Resource after adding the finalizer + // so that we have the latest state of the resource on the cluster and we will avoid + // raise the issue "the object has been modified, please apply + // your changes to the latest version and try again" which would re-trigger the reconciliation + // if we try to update it again in the following operations + if err := r.Get(ctx, req.NamespacedName, s3InstanceResource); err != nil { + logger.Error(err, "Failed to re-fetch GrafanaInstance", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, err } + } // 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 { logger.Info("s3InstanceResource have been marked for deletion") - return r.handleS3InstanceDeletion(ctx, s3InstanceResource) + return r.handleS3InstanceDeletion(ctx, req, s3InstanceResource) } - // Add finalizer for this CR - if !controllerutil.ContainsFinalizer(s3InstanceResource, s3InstanceFinalizer) { - logger.Info("adding finalizer to s3Instance") + // Reconciliation starts here + return r.handleReconciliation(ctx, req, s3InstanceResource) - controllerutil.AddFinalizer(s3InstanceResource, s3InstanceFinalizer) - err = r.Update(ctx, s3InstanceResource) - if err != nil { - logger.Error(err, "an error occurred when adding finalizer from s3Instance", "s3Instance", s3InstanceResource.Name) - return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceFinalizerAddFailed", - fmt.Sprintf("An error occurred when attempting to add the finalizer from s3Instance %s", s3InstanceResource.Name), err) - } - } +} + +func (r *S3InstanceReconciler) handleReconciliation(ctx context.Context, req reconcile.Request, s3InstanceResource *s3v1alpha1.S3Instance) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + _, found := r.S3ClientCache.Get(s3InstanceResource.Namespace + "/" + s3InstanceResource.Name) - // Check s3Instance existence - _, found := r.S3ClientCache.Get(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") - return r.handleS3InstanceCreation(ctx, s3InstanceResource) + return r.handleS3InstanceCreation(ctx, req, s3InstanceResource) } logger.Info("this S3Instance already exists and will be reconciled") - return r.handleS3InstanceUpdate(ctx, s3InstanceResource) - + return r.handleS3InstanceUpdate(ctx, req, s3InstanceResource) } -func (r *S3InstanceReconciler) handleS3InstanceUpdate(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (reconcile.Result, error) { +func (r *S3InstanceReconciler) handleS3InstanceUpdate(ctx context.Context, req ctrl.Request, 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.getS3InstanceAccessSecret(ctx, s3InstanceResource) + if err != nil { + logger.Error(err, "Could not get s3Instance auth secret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretRef, "NamespacedName", req.NamespacedName.String()) + return r.SetReconciledCondition(ctx, req, s3InstanceResource, s3v1alpha1.Unreachable, + "Failed to fetch S3Instance's secret ", err) + } - s3InstanceSecretSecretExpected, err := r.getS3InstanceSecret(ctx, s3InstanceResource) + 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, "S3InstanceUpdateFailed", - fmt.Sprintf("Updating secret of S3Instance %s has failed", s3InstanceResource.Name), err) + logger.Error(err, "Could not get s3Instance cert secret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretRef, "NamespacedName", req.NamespacedName.String()) + return r.SetReconciledCondition(ctx, req, s3InstanceResource, s3v1alpha1.Unreachable, + "Failed to fetch S3Instance's cert secret ", 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"]) { - 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) - return r.handleS3InstanceCreation(ctx, s3InstanceResource) + if s3Config.S3Provider != s3InstanceResource.Spec.S3Provider || s3Config.S3Url != s3InstanceResource.Spec.Url || s3Config.Region != s3InstanceResource.Spec.Region || slices.Equal(s3Config.AllowedNamespaces, allowedNamepaces) || slices.Equal(s3Config.CaCertificatesBase64, []string{string(s3InstanceCaCertSecretExpected.Data["ca.crt"])}) || s3Config.AccessKey != string(s3InstanceSecretSecretExpected.Data["S3_ACCESS_KEY"]) || s3Config.SecretKey != string(s3InstanceSecretSecretExpected.Data["S3_SECRET_KEY"]) || s3Config.BucketDeletionEnabled != s3InstanceResource.Spec.BucketDeletionEnabled || s3Config.S3UserDeletionEnabled != s3InstanceResource.Spec.S3UserDeletionEnabled || s3Config.PolicyDeletionEnabled != s3InstanceResource.Spec.PolicyDeletionEnabled || s3Config.PathDeletionEnabled != s3InstanceResource.Spec.PathDeletionEnabled { + logger.Info("Instance in cache not equal to expected , cache will be prune and instance recreate") + r.S3ClientCache.Remove(s3ClientName) + return r.handleS3InstanceCreation(ctx, req, s3InstanceResource) } - return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorSucceeded", metav1.ConditionTrue, "S3InstanceUpdated", - fmt.Sprintf("The S3Instance %s was updated was reconcile successfully", s3InstanceResource.Name), nil) + return r.SetReconciledCondition(ctx, req, s3InstanceResource, s3v1alpha1.Reconciled, "S3Instance instance reconciled", nil) } -func (r *S3InstanceReconciler) handleS3InstanceCreation(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (reconcile.Result, error) { +func (r *S3InstanceReconciler) handleS3InstanceCreation(ctx context.Context, req ctrl.Request, 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", - fmt.Sprintf("Getting secret of S3s3Instance %s has failed", s3InstanceResource.Name), err) + logger.Error(err, "Could not get s3Instance auth secret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretRef, "NamespacedName", req.NamespacedName.String()) + return r.SetReconciledCondition(ctx, req, s3InstanceResource, s3v1alpha1.Unreachable, + "Failed to fetch S3Instance's secret ", err) + } + s3InstanceCaCertSecret, err := r.getS3InstanceCaCertSecret(ctx, s3InstanceResource) + if err != nil { + logger.Error(err, "Could not get s3Instance cert secret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretRef, "NamespacedName", req.NamespacedName.String()) + return r.SetReconciledCondition(ctx, req, s3InstanceResource, s3v1alpha1.Unreachable, + "Failed to fetch S3Instance's cert secret ", err) } - 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} + 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"]), S3Url: s3InstanceResource.Spec.Url, Region: s3InstanceResource.Spec.Region, AllowedNamespaces: allowedNamepaces, CaCertificatesBase64: []string{string(s3InstanceCaCertSecret.Data["ca.crt"])}, BucketDeletionEnabled: s3InstanceResource.Spec.BucketDeletionEnabled, S3UserDeletionEnabled: s3InstanceResource.Spec.S3UserDeletionEnabled, PolicyDeletionEnabled: s3InstanceResource.Spec.PolicyDeletionEnabled, PathDeletionEnabled: s3InstanceResource.Spec.PathDeletionEnabled} + 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) + logger.Error(err, "Could not generate s3Instance", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretRef, "NamespacedName", req.NamespacedName.String()) + return r.SetReconciledCondition(ctx, req, s3InstanceResource, s3v1alpha1.CreationFailure, + "Failed to generate S3Instance ", 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) + return r.SetReconciledCondition(ctx, req, s3InstanceResource, s3v1alpha1.Reconciled, "S3Instance instance reconciled", nil) } -func (r *S3InstanceReconciler) handleS3InstanceDeletion(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (reconcile.Result, error) { +func (r *S3InstanceReconciler) handleS3InstanceDeletion(ctx context.Context, req ctrl.Request, s3InstanceResource *s3v1alpha1.S3Instance) (reconcile.Result, error) { logger := log.FromContext(ctx) if controllerutil.ContainsFinalizer(s3InstanceResource, s3InstanceFinalizer) { - // Run finalization logic for S3InstanceFinalizer. If the finalization logic fails, don't remove the finalizer so that we can retry during the next reconciliation. + logger.Info("Performing Finalizer Operations for S3Instance before delete CR", "Namespace", s3InstanceResource.GetNamespace(), "Name", s3InstanceResource.GetName()) + + ctrlResult, err := r.checkS3InstanceReferencesInBucket(ctx, req, s3InstanceResource) + if err != nil { + return ctrlResult, err + } + + ctrlResult, err = r.checkS3InstanceReferencesInPolicy(ctx, req, s3InstanceResource) + if err != nil { + return ctrlResult, err + } + + ctrlResult, err = r.checkS3InstanceReferencesInPath(ctx, req, s3InstanceResource) + if err != nil { + return ctrlResult, err + } + + ctrlResult, err = r.checkS3InstanceReferencesInS3User(ctx, req, s3InstanceResource) + if err != nil { + return ctrlResult, err + } + if err := r.finalizeS3Instance(ctx, s3InstanceResource); err != nil { logger.Error(err, "an error occurred when attempting to finalize the s3Instance", "s3Instance", s3InstanceResource.Name) - return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceFinalizeFailed", + return r.SetReconciledCondition(ctx, req, s3InstanceResource, s3v1alpha1.DeletionFailure, fmt.Sprintf("An error occurred when attempting to delete s3Instance %s", s3InstanceResource.Name), err) } //Remove s3InstanceFinalizer. Once all finalizers have been removed, the object will be deleted. - controllerutil.RemoveFinalizer(s3InstanceResource, s3InstanceFinalizer) + if ok := controllerutil.RemoveFinalizer(s3InstanceResource, s3InstanceFinalizer); !ok { + logger.Error(err, "Failed to remove finalizer for S3Instance", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{Requeue: true}, nil + } + // Unsure why the behavior is different to that of bucket/policy/path controllers, but it appears // calling r.Update() for adding/removal of finalizer is not necessary (an update event is generated // with the call to AddFinalizer/RemoveFinalizer), and worse, causes "freshness" problem (with the // "the object has been modified; please apply your changes to the latest version and try again" error) - err := r.Update(ctx, s3InstanceResource) - if err != nil { - logger.Error(err, "Failed to remove finalizer.") - return r.setS3InstanceStatusConditionAndUpdate(ctx, s3InstanceResource, "OperatorFailed", metav1.ConditionFalse, "S3InstanceFinalizerRemovalFailed", - fmt.Sprintf("An error occurred when attempting to remove the finalizer from s3Instance %s", s3InstanceResource.Name), err) + if err := r.Update(ctx, s3InstanceResource); err != nil { + logger.Error(err, "Failed to remove finalizer for S3Instance", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, err } } return ctrl.Result{}, nil @@ -212,32 +281,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. @@ -248,70 +297,170 @@ func (r *S3InstanceReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *S3InstanceReconciler) setS3InstanceStatusConditionAndUpdate(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance, conditionType string, status metav1.ConditionStatus, reason string, message string, srcError error) (ctrl.Result, error) { +func (r *S3InstanceReconciler) SetReconciledCondition(ctx context.Context, req ctrl.Request, s3InstanceResource *s3v1alpha1.S3Instance, reason string, message string, err error) (ctrl.Result, error) { logger := log.FromContext(ctx) - // We moved away from meta.SetStatusCondition, as the implementation did not allow for updating - // lastTransitionTime if a Condition (as identified by Reason instead of Type) was previously - // obtained and updated to again. - s3InstanceResource.Status.Conditions = utils.UpdateConditions(s3InstanceResource.Status.Conditions, metav1.Condition{ - Type: conditionType, - Status: status, - Reason: reason, - LastTransitionTime: metav1.NewTime(time.Now()), - Message: message, - ObservedGeneration: s3InstanceResource.GetGeneration(), - }) - - err := r.Status().Update(ctx, s3InstanceResource) + var changed bool + if err != nil { - logger.Error(err, "an error occurred while updating the status of the S3Instance resource") - return ctrl.Result{}, utilerrors.NewAggregate([]error{err, srcError}) + logger.Error(err, message, "NamespacedName", req.NamespacedName.String()) + changed = meta.SetStatusCondition(&s3InstanceResource.Status.Conditions, metav1.Condition{Type: s3v1alpha1.ConditionReconciled, + Status: metav1.ConditionFalse, ObservedGeneration: s3InstanceResource.Generation, Reason: reason, + Message: fmt.Sprintf("%s: %s", message, err)}) + + } else { + logger.Info(message, "NamespacedName", req.NamespacedName.String()) + changed = meta.SetStatusCondition(&s3InstanceResource.Status.Conditions, metav1.Condition{Type: s3v1alpha1.ConditionReconciled, + Status: metav1.ConditionTrue, ObservedGeneration: s3InstanceResource.Generation, Reason: reason, + Message: message}) + } + + if changed { + if errStatusUpdate := r.Status().Update(ctx, s3InstanceResource); errStatusUpdate != nil { + logger.Error(errStatusUpdate, "Failed to update GrafanaInstance status", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, errStatusUpdate + } } - return ctrl.Result{}, srcError + + return ctrl.Result{RequeueAfter: r.ReconcilePeriod}, err } 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) { - logger := log.FromContext(ctx) +func (r *S3InstanceReconciler) getS3InstanceAccessSecret(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (corev1.Secret, error) { + s3InstanceSecret := &corev1.Secret{} + err := r.Get(ctx, types.NamespacedName{Namespace: s3InstanceResource.Namespace, Name: s3InstanceResource.Spec.SecretRef}, s3InstanceSecret) + if err != nil { + if k8sapierrors.IsNotFound(err) { + return *s3InstanceSecret, fmt.Errorf("secret %s not found in namespace %s", s3InstanceResource.Spec.SecretRef, s3InstanceResource.Namespace) + } + return *s3InstanceSecret, err + } + return *s3InstanceSecret, nil +} - secretsList := &corev1.SecretList{} - s3InstanceSecret := corev1.Secret{} +func (r *S3InstanceReconciler) getS3InstanceCaCertSecret(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (corev1.Secret, error) { - err := r.List(ctx, secretsList, client.InNamespace(s3InstanceResource.Namespace)) + s3InstanceCaCertSecret := &corev1.Secret{} + + if s3InstanceResource.Spec.CaCertSecretRef == "" { + return *s3InstanceCaCertSecret, nil + } + + err := r.Get(ctx, types.NamespacedName{Namespace: s3InstanceResource.Namespace, Name: s3InstanceResource.Spec.SecretRef}, s3InstanceCaCertSecret) if err != nil { - logger.Error(err, "An error occurred while listing the secrets in s3instance's namespace") - return s3InstanceSecret, fmt.Errorf("SecretListingFailed") + if k8sapierrors.IsNotFound(err) { + return *s3InstanceCaCertSecret, fmt.Errorf("secret %s not found in namespace %s", s3InstanceResource.Spec.SecretRef, s3InstanceResource.Namespace) + } + return *s3InstanceCaCertSecret, err } + return *s3InstanceCaCertSecret, nil +} - if len(secretsList.Items) == 0 { - logger.Info("The s3instance's namespace doesn't appear to contain any secret") - return s3InstanceSecret, nil +func (r *S3InstanceReconciler) checkS3InstanceReferencesInBucket(ctx context.Context, req ctrl.Request, s3InstanceResource *s3v1alpha1.S3Instance) (ctrl.Result, error) { + bucketLists := s3v1alpha1.BucketList{} + err := r.List(ctx, &bucketLists) + found := 0 + if err != nil { + return r.SetReconciledCondition(ctx, req, s3InstanceResource, + s3v1alpha1.DeletionFailure, "Failed retrieve Buckets", err) } - // In all the secrets inside the s3instance's namespace, one should have a name equal to - // the S3InstanceSecretRefName field. - s3InstanceSecretName := s3InstanceResource.Spec.SecretName - // cmp.Or takes the first non "zero" value, see https://pkg.go.dev/cmp#Or - for _, secret := range secretsList.Items { - if secret.Name == s3InstanceSecretName { - s3InstanceSecret = secret - break + for _, bucket := range bucketLists.Items { + if bucket.Namespace == s3InstanceResource.Namespace && bucket.Spec.S3InstanceRef == s3InstanceResource.Name { + found++ + } else if bucket.Spec.S3InstanceRef == s3InstanceResource.Namespace+"/"+s3InstanceResource.Name { + found++ } } - return s3InstanceSecret, nil + if found > 0 { + return r.SetReconciledCondition(ctx, req, s3InstanceResource, + s3v1alpha1.DeletionFailure, "Unable to delete s3Instance", fmt.Errorf("found %d bucket which use this s3Instance", found)) + } + return ctrl.Result{}, nil +} + +func (r *S3InstanceReconciler) checkS3InstanceReferencesInPolicy(ctx context.Context, req ctrl.Request, s3InstanceResource *s3v1alpha1.S3Instance) (ctrl.Result, error) { + policyLists := s3v1alpha1.PolicyList{} + err := r.List(ctx, &policyLists) + found := 0 + if err != nil { + return r.SetReconciledCondition(ctx, req, s3InstanceResource, + s3v1alpha1.DeletionFailure, "Failed retrieve Policies", err) + } + + for _, policy := range policyLists.Items { + if policy.Namespace == s3InstanceResource.Namespace && policy.Spec.S3InstanceRef == s3InstanceResource.Name { + found++ + } else if policy.Spec.S3InstanceRef == s3InstanceResource.Namespace+"/"+s3InstanceResource.Name { + found++ + } + } + + if found > 0 { + return r.SetReconciledCondition(ctx, req, s3InstanceResource, + s3v1alpha1.DeletionFailure, "Unable to delete s3Instance", fmt.Errorf("found %d policy which use this s3Instance", found)) + } + return ctrl.Result{}, nil +} + +func (r *S3InstanceReconciler) checkS3InstanceReferencesInS3User(ctx context.Context, req ctrl.Request, s3InstanceResource *s3v1alpha1.S3Instance) (ctrl.Result, error) { + s3UserList := s3v1alpha1.S3UserList{} + err := r.List(ctx, &s3UserList) + found := 0 + if err != nil { + return r.SetReconciledCondition(ctx, req, s3InstanceResource, + s3v1alpha1.DeletionFailure, "Failed retrieve s3Users", err) + } + + for _, policy := range s3UserList.Items { + if policy.Namespace == s3InstanceResource.Namespace && policy.Spec.S3InstanceRef == s3InstanceResource.Name { + found++ + } else if policy.Spec.S3InstanceRef == s3InstanceResource.Namespace+"/"+s3InstanceResource.Name { + found++ + } + } + + if found > 0 { + return r.SetReconciledCondition(ctx, req, s3InstanceResource, + s3v1alpha1.DeletionFailure, "Unable to delete s3Instance", fmt.Errorf("found %d s3User which use this s3Instance", found)) + } + return ctrl.Result{}, nil +} + +func (r *S3InstanceReconciler) checkS3InstanceReferencesInPath(ctx context.Context, req ctrl.Request, s3InstanceResource *s3v1alpha1.S3Instance) (ctrl.Result, error) { + pathList := s3v1alpha1.PathList{} + err := r.List(ctx, &pathList) + found := 0 + if err != nil { + return r.SetReconciledCondition(ctx, req, s3InstanceResource, + s3v1alpha1.DeletionFailure, "Failed retrieve paths", err) + } + + for _, policy := range pathList.Items { + if policy.Namespace == s3InstanceResource.Namespace && policy.Spec.S3InstanceRef == s3InstanceResource.Name { + found++ + } else if policy.Spec.S3InstanceRef == s3InstanceResource.Namespace+"/"+s3InstanceResource.Name { + found++ + } + } + + if found > 0 { + return r.SetReconciledCondition(ctx, req, s3InstanceResource, + s3v1alpha1.DeletionFailure, "Unable to delete s3Instance", fmt.Errorf("found %d path which use this s3Instance", found)) + } + return ctrl.Result{}, nil } diff --git a/controllers/user_controller.go b/controllers/user_controller.go index c5bd4d8..d4564d1 100644 --- a/controllers/user_controller.go +++ b/controllers/user_controller.go @@ -50,9 +50,7 @@ type S3UserReconciler struct { client.Client Scheme *runtime.Scheme S3ClientCache *s3ClientCache.S3ClientCache - S3UserDeletion bool OverrideExistingSecret bool - S3LabelSelectorValue string } const ( @@ -83,19 +81,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 +101,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) + s3Client, err := r.getS3InstanceForObject(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 +134,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) + s3Client, err := r.getS3InstanceForObject(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 +265,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) + s3Client, err := r.getS3InstanceForObject(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 { @@ -395,9 +398,8 @@ func (r *S3UserReconciler) handleS3NewUser(ctx context.Context, userResource *s3 func (r *S3UserReconciler) addPoliciesToUser(ctx context.Context, userResource *s3v1alpha1.S3User) error { logger := log.FromContext(ctx) // Create S3Client - s3Client, err := r.getS3InstanceForObject(ctx, userResource) + s3Client, err := r.getS3InstanceForObject(userResource) if err != nil { - logger.Error(err, "an error occurred while getting s3Client") return err } policies := userResource.Spec.Policies @@ -557,12 +559,12 @@ func (r *S3UserReconciler) setS3UserStatusConditionAndUpdate(ctx context.Context func (r *S3UserReconciler) finalizeS3User(ctx context.Context, userResource *s3v1alpha1.S3User) error { logger := log.FromContext(ctx) // Create S3Client - s3Client, err := r.getS3InstanceForObject(ctx, userResource) + s3Client, err := r.getS3InstanceForObject(userResource) if err != nil { logger.Error(err, "an error occurred while getting s3Client") return err } - if r.S3UserDeletion { + if s3Client.GetConfig().S3UserDeletionEnabled { return s3Client.DeleteUser(userResource.Spec.AccessKey) } return nil @@ -607,25 +609,6 @@ func (r *S3UserReconciler) newSecretForCR(ctx context.Context, userResource *s3v } -func (r *S3UserReconciler) getS3InstanceForObject(ctx context.Context, userResource *s3v1alpha1.S3User) (factory.S3Client, error) { - logger := log.FromContext(ctx) - if userResource.Spec.S3InstanceRef == "" { - 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") - 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) - if !found { - err := &s3ClientCache.S3ClientCacheError{Reason: fmt.Sprintf("S3InstanceRef: %s,not found in cache", userResource.Spec.S3InstanceRef)} - logger.Error(err, "No client was found") - return nil, err - } - return s3Client, nil - } +func (r *S3UserReconciler) getS3InstanceForObject(userResource *s3v1alpha1.S3User) (factory.S3Client, error) { + return r.S3ClientCache.GetS3Instance(userResource.Name, userResource.Namespace, userResource.Spec.S3InstanceRef) } diff --git a/go.mod b/go.mod index e781b36..9b06ae5 100644 --- a/go.mod +++ b/go.mod @@ -1,98 +1,106 @@ module github.com/InseeFrLab/s3-operator -go 1.22 +go 1.22.0 + +toolchain go1.22.2 + +require ( + github.com/minio/madmin-go/v3 v3.0.70 + github.com/minio/minio-go/v7 v7.0.77 + github.com/onsi/ginkgo/v2 v2.19.0 + github.com/onsi/gomega v1.33.1 + go.uber.org/zap v1.27.0 + k8s.io/api v0.31.1 + k8s.io/apimachinery v0.31.1 + k8s.io/client-go v0.31.1 + sigs.k8s.io/controller-runtime v0.19.0 +) require ( - github.com/minio/madmin-go/v3 v3.0.34 - github.com/minio/minio-go/v7 v7.0.64 - github.com/onsi/ginkgo/v2 v2.11.0 - github.com/onsi/gomega v1.27.10 - go.uber.org/zap v1.25.0 - k8s.io/api v0.28.3 - k8s.io/apimachinery v0.28.3 - k8s.io/client-go v0.28.3 - sigs.k8s.io/controller-runtime v0.16.3 + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/prometheus/prometheus v0.54.1 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/x448/float16 v0.8.4 // indirect ) 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/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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 - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.2.4 // indirect - github.com/go-logr/zapr v1.2.4 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - 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/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // 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 - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.16.7 // indirect - github.com/klauspost/cpuid/v2 v2.2.5 // indirect - github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de // indirect + github.com/klauspost/compress v1.17.10 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/sha256-simd v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/philhofer/fwd v1.1.2 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect - github.com/prometheus/client_golang v1.16.0 // indirect - github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.44.0 // indirect - github.com/prometheus/procfs v0.10.1 // indirect - github.com/prometheus/prom2json v1.3.3 // indirect - github.com/rs/xid v1.5.0 // indirect - github.com/safchain/ethtool v0.3.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus/client_golang v1.20.4 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.60.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/prom2json v1.4.1 // indirect + github.com/rs/xid v1.6.0 // indirect + github.com/safchain/ethtool v0.4.1 // indirect github.com/secure-io/sio-go v0.3.1 // indirect - github.com/shirou/gopsutil/v3 v3.23.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/tinylib/msgp v1.1.8 // indirect - github.com/tklauser/go-sysconf v0.3.11 // indirect - github.com/tklauser/numcpus v0.6.0 // indirect - github.com/yusufpapurcu/wmi v1.2.2 // indirect + github.com/tinylib/msgp v1.2.2 // indirect + github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sync v0.2.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/term v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.9.3 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/time v0.6.0 // indirect + golang.org/x/tools v0.25.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.28.3 // indirect - k8s.io/component-base v0.28.3 // indirect - k8s.io/klog/v2 v2.100.1 // indirect - k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect - k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect + k8s.io/apiextensions-apiserver v0.31.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect + k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 9790a39..bb544ef 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,46 @@ -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= -github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -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/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +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/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 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= @@ -44,61 +48,51 @@ github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= +github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= -github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de h1:V53FWzU6KAZVi1tPp5UIsMoUWJ2/PNwYIDXnu7QuBCE= -github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= +github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/minio/madmin-go/v3 v3.0.34 h1:MGPQYIWm52liSubofK24FhrznPYnRpQrDNddZJEyBPA= -github.com/minio/madmin-go/v3 v3.0.34/go.mod h1:4QN2NftLSV7MdlT50dkrenOMmNVHluxTvlqJou3hte8= +github.com/minio/madmin-go/v3 v3.0.70 h1:zrFCXLcV6PR74JC0yytK4Dk2qsaCV8kXQoPTvcusR2k= +github.com/minio/madmin-go/v3 v3.0.70/go.mod h1:TOTc96ZkMknNhl+ReO/V68bQfgRGfH+8iy7YaDzHdXA= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.64 h1:Zdza8HwOzkld0ZG/og50w56fKi6AAyfqfifmasD9n2Q= -github.com/minio/minio-go/v7 v7.0.64/go.mod h1:R4WVUR6ZTedlCcGwZRauLMIKjgyaWxhs4Mqi/OMPmEc= -github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= -github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/minio/minio-go/v7 v7.0.77 h1:GaGghJRg9nwDVlNbwYjSDJT1rqltQkBFDsypWX1v3Bw= +github.com/minio/minio-go/v7 v7.0.77/go.mod h1:AVM3IUN6WwKzmwBxVdjzhH8xq+f57JSbbvzqvUzR6eg= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -106,211 +100,157 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= -github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= -github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= -github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= -github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= -github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo= -github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= -github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= +github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/prometheus/prom2json v1.4.1 h1:7McxdrHgPEOtMwWjkKtd0v5AhpR2Q6QAnlHKVxq0+tQ= +github.com/prometheus/prom2json v1.4.1/go.mod h1:CzOQykSKFxXuC7ELUZHOHQvwKesQ3eN0p2PWLhFitQM= +github.com/prometheus/prometheus v0.54.1 h1:vKuwQNjnYN2/mDoWfHXDhAsz/68q/dQDb+YbcEqU7MQ= +github.com/prometheus/prometheus v0.54.1/go.mod h1:xlLByHhk2g3ycakQGrMaU8K7OySZx98BzeCR99991NY= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/safchain/ethtool v0.4.1 h1:S6mEleTADqgynileXoiapt/nKnatyR6bmIHoF+h2ADo= +github.com/safchain/ethtool v0.4.1/go.mod h1:XLLnZmy4OCRTkksP/UiMjij96YmIsBfmBQcs7H6tA48= github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc= github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= -github.com/shirou/gopsutil/v3 v3.23.1 h1:a9KKO+kGLKEvcPIs4W62v0nu3sciVDOOOPUD0Hz7z/4= -github.com/shirou/gopsutil/v3 v3.23.1/go.mod h1:NN6mnm5/0k8jw4cBfCnJtr5L7ErOTg18tMNpgFkn0hA= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= -github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= -github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= -github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= -github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms= -github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tinylib/msgp v1.2.2 h1:iHiBE1tJQwFI740SPEPkGE8cfhNfrqOYRlH450BnC/4= +github.com/tinylib/msgp v1.2.2/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= +github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= +github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= +github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= -github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= -go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM= -golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= -k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= -k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= -k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= -k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= -k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= -k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= -k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= -k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= -k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= -k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= -k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= -sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU= +k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI= +k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40= +k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ= +k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= +k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0= +k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 h1:1dWzkmJrrprYvjGwh9kEUxmcUV/CtNU8QM7h1FLWQOo= +k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI= +k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= +sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/s3/factory/interface.go b/internal/s3/factory/interface.go index dbd14b2..32b6461 100644 --- a/internal/s3/factory/interface.go +++ b/internal/s3/factory/interface.go @@ -2,7 +2,6 @@ package factory import ( "fmt" - "os" "github.com/minio/madmin-go/v3" @@ -40,14 +39,17 @@ type S3Client interface { } type S3Config struct { - S3Provider string - S3UrlEndpoint string - Region string - AccessKey string - SecretKey string - UseSsl bool - CaCertificatesBase64 []string - CaBundlePath string + S3Provider string + S3Url string + Region string + AccessKey string + SecretKey string + CaCertificatesBase64 []string + AllowedNamespaces []string + BucketDeletionEnabled bool + S3UserDeletionEnabled bool + PathDeletionEnabled bool + PolicyDeletionEnabled bool } func GenerateS3Client(s3Provider string, S3Config *S3Config) (S3Client, error) { @@ -55,35 +57,7 @@ func GenerateS3Client(s3Provider string, S3Config *S3Config) (S3Client, error) { return newMockedS3Client(), nil } if s3Provider == "minio" { - return newMinioS3Client(S3Config), nil - } - 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) { - // 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. - - var accessKeyFromEnvIfAvailable = os.Getenv("S3_ACCESS_KEY") - if accessKeyFromEnvIfAvailable == "" { - accessKeyFromEnvIfAvailable = accessKey - } - var secretKeyFromEnvIfAvailable = os.Getenv("S3_SECRET_KEY") - if secretKeyFromEnvIfAvailable == "" { - secretKeyFromEnvIfAvailable = secretKey - } - - if s3Provider == "" || s3UrlEndpoint == "" || accessKeyFromEnvIfAvailable == "" || secretKeyFromEnvIfAvailable == "" { - s3Logger.Info("No default S3Client to create") - return nil, nil - } - - if s3Provider == "mockedS3Provider" { - return newMockedS3Client(), nil - } - if s3Provider == "minio" { - S3Config := &S3Config{S3Provider: s3Provider, S3UrlEndpoint: s3UrlEndpoint, Region: region, AccessKey: accessKeyFromEnvIfAvailable, SecretKey: secretKeyFromEnvIfAvailable, UseSsl: useSsl, CaCertificatesBase64: caCertificatesBase64, CaBundlePath: caBundlePath} - return newMinioS3Client(S3Config), nil + return newMinioS3Client(S3Config) } 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..8fc209f 100644 --- a/internal/s3/factory/minioS3Client.go +++ b/internal/s3/factory/minioS3Client.go @@ -6,8 +6,9 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" + "fmt" "net/http" - "os" + neturl "net/url" "strings" "github.com/minio/madmin-go/v3" @@ -21,87 +22,101 @@ type MinioS3Client struct { adminClient madmin.AdminClient } -func newMinioS3Client(S3Config *S3Config) *MinioS3Client { +func newMinioS3Client(S3Config *S3Config) (*MinioS3Client, error) { s3Logger.Info("creating minio clients (regular and admin)") + minioClient, err := generateMinioClient(S3Config.S3Url, S3Config.AccessKey, S3Config.SecretKey, S3Config.Region, S3Config.CaCertificatesBase64) + if err != nil { + s3Logger.Error(err, "an error occurred while creating a new minio client") + return nil, err + } + adminClient, err := generateAdminMinioClient(S3Config.S3Url, S3Config.AccessKey, S3Config.SecretKey, S3Config.Region, S3Config.CaCertificatesBase64) + if err != nil { + s3Logger.Error(err, "an error occurred while creating a new minio admin client") + return nil, err + } + return &MinioS3Client{*S3Config, *minioClient, *adminClient}, nil +} + +func generateMinioClient(url string, accessKey string, secretKey string, region string, caCertificateBase64 []string) (*minio.Client, error) { + hostname, isSSL, err := extractHostAndScheme(url) + if err != nil { + s3Logger.Error(err, "an error occurred while creating a new minio client") + return nil, err + } minioOptions := &minio.Options{ - Creds: credentials.NewStaticV4(S3Config.AccessKey, S3Config.SecretKey, ""), - Region: S3Config.Region, - Secure: S3Config.UseSsl, - } - - // Preparing the tlsConfig to support custom CA if configured - // See also : - // - https://pkg.go.dev/github.com/minio/minio-go/v7@v7.0.52#Options - // - https://pkg.go.dev/net/http#RoundTripper - // - https://youngkin.github.io/post/gohttpsclientserver/#create-the-client - // - https://forfuncsake.github.io/post/2017/08/trust-extra-ca-cert-in-go-app/ - // Appending content directly, from a base64-encoded, PEM format CA certificate - // Variant : if S3Config.CaBundlePath was a string[] - // for _, caCertificateFilePath := range S3Config.S3Config.CaBundlePaths { - // caCert, err := os.ReadFile(caCertificateFilePath) - // if err != nil { - // log.Fatalf("Error opening CA cert file %s, Error: %s", caCertificateFilePath, err) - // } - // rootCAs.AppendCertsFromPEM([]byte(caCert)) - // } - addTransportOptions(S3Config, minioOptions) - - minioClient, err := minio.New(S3Config.S3UrlEndpoint, minioOptions) + Creds: credentials.NewStaticV4(accessKey, secretKey, ""), + Region: region, + Secure: isSSL, + } + + if len(caCertificateBase64) > 0 { + addTlsClientConfigToMinioOptions(caCertificateBase64, minioOptions) + } + + minioClient, err := minio.New(hostname, minioOptions) if err != nil { s3Logger.Error(err, "an error occurred while creating a new minio client") + return nil, err } + return minioClient, nil +} - adminClient, err := madmin.New(S3Config.S3UrlEndpoint, S3Config.AccessKey, S3Config.SecretKey, S3Config.UseSsl) +func generateAdminMinioClient(url string, accessKey string, secretKey string, region string, caCertificateBase64 []string) (*madmin.AdminClient, error) { + hostname, isSSL, err := extractHostAndScheme(url) if err != nil { - s3Logger.Error(err, "an error occurred while creating a new minio admin client") + s3Logger.Error(err, "an error occurred while creating a new minio client") + return nil, err + } + + minioAdminClient, err := madmin.New(hostname, accessKey, secretKey, isSSL) + if err != nil { + s3Logger.Error(err, "an error occurred while creating a new minio client") + return nil, err } - // Getting the custom root CA (if any) from the "regular" client's Transport - adminClient.SetCustomTransport(minioOptions.Transport) - return &MinioS3Client{*S3Config, *minioClient, *adminClient} -} + minioOptions := &minio.Options{ + Region: region, + Secure: isSSL, + } -func addTransportOptions(S3Config *S3Config, minioOptions *minio.Options) { - if len(S3Config.CaCertificatesBase64) > 0 { + if len(caCertificateBase64) > 0 { + addTlsClientConfigToMinioOptions(caCertificateBase64, minioOptions) + } - rootCAs, _ := x509.SystemCertPool() - if rootCAs == nil { - rootCAs = x509.NewCertPool() - } + minioAdminClient.SetCustomTransport(minioOptions.Transport) - for _, caCertificateBase64 := range S3Config.CaCertificatesBase64 { - decodedCaCertificate, err := base64.StdEncoding.DecodeString(caCertificateBase64) - if err != nil { - s3Logger.Error(err, "an error occurred while parsing a base64-encoded CA certificate") - } + return minioAdminClient, nil +} - rootCAs.AppendCertsFromPEM(decodedCaCertificate) - } +func extractHostAndScheme(url string) (string, bool, error) { + parsedURL, err := neturl.Parse(url) + if err != nil { + return "", false, fmt.Errorf("cannot detect if url use ssl or not") + } + return parsedURL.Hostname(), parsedURL.Scheme == "https", nil +} - minioOptions.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: rootCAs, - }, - } - } else if len(S3Config.CaBundlePath) > 0 { +func addTlsClientConfigToMinioOptions(caCertificatesBase64 []string, minioOptions *minio.Options) { + rootCAs, _ := x509.SystemCertPool() - rootCAs, _ := x509.SystemCertPool() - if rootCAs == nil { - rootCAs = x509.NewCertPool() - } + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } - caCert, err := os.ReadFile(S3Config.CaBundlePath) + for _, _caCertificateBase64 := range caCertificatesBase64 { + decodedCaCertificate, err := base64.StdEncoding.DecodeString(_caCertificateBase64) if err != nil { - s3Logger.Error(err, "an error occurred while reading a CA certificates bundle file") + s3Logger.Error(err, "an error occurred while parsing a base64-encoded CA certificate") } - rootCAs.AppendCertsFromPEM([]byte(caCert)) - minioOptions.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: rootCAs, - }, - } + rootCAs.AppendCertsFromPEM(decodedCaCertificate) + } + + minioOptions.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: rootCAs, + }, } } @@ -325,17 +340,12 @@ func (minioS3Client *MinioS3Client) GetUserPolicies(accessKey string) ([]string, func (minioS3Client *MinioS3Client) CheckUserCredentialsValid(name string, accessKey string, secretKey string) (bool, error) { s3Logger.Info("Check credentials for user", "user", name, "accessKey", accessKey) - minioTestClientOptions := &minio.Options{ - Creds: credentials.NewStaticV4(accessKey, secretKey, ""), - Region: minioS3Client.s3Config.Region, - Secure: minioS3Client.s3Config.UseSsl, - } - addTransportOptions(&minioS3Client.s3Config, minioTestClientOptions) - minioTestClient, err := minio.New(minioS3Client.s3Config.S3UrlEndpoint, minioTestClientOptions) + + minioTestClient, err := generateMinioClient(minioS3Client.s3Config.S3Url, accessKey, secretKey, minioS3Client.s3Config.Region, minioS3Client.s3Config.CaCertificatesBase64) if err != nil { s3Logger.Error(err, "An error occurred while creating a new Minio test client") + return false, err } - _, err = minioTestClient.ListBuckets(context.Background()) if err != nil { errAsResponse := minio.ToErrorResponse(err) diff --git a/internal/s3/s3ClientCache.go b/internal/s3/s3ClientCache.go index e8b5851..4b213e8 100644 --- a/internal/s3/s3ClientCache.go +++ b/internal/s3/s3ClientCache.go @@ -2,9 +2,18 @@ package S3ClientCache import ( "fmt" + "os" + "strings" "sync" + ctrl "sigs.k8s.io/controller-runtime" + "github.com/InseeFrLab/s3-operator/internal/s3/factory" + "github.com/InseeFrLab/s3-operator/internal/utils" +) + +var ( + logger = ctrl.Log.WithValues("logger", "s3clientCache") ) // Cache is a basic in-memory key-value cache implementation. @@ -15,6 +24,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 +34,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 +43,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 +53,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 +73,77 @@ 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 +} + +func (s3ClientCache *S3ClientCache) GetS3Instance(ressourceName string, ressourceNamespace string, ressourceS3InstanceRef string) (factory.S3Client, error) { + if ressourceS3InstanceRef == "" { + logger.Info("Resource doesn't refer to s3Instance, failback to default one") + operatorNamespace := os.Getenv("POD_NAMESPACE") + if operatorNamespace == "" { + err := &S3ClientNotFound{Reason: "Client not found"} + logger.Error(err, "Client \"default\" was not found") + return nil, err + } + s3Client, found := s3ClientCache.Get(operatorNamespace + "/default") + if !found { + err := &S3ClientNotFound{Reason: "Client not found"} + logger.Error(err, "Client \"default\" was not found") + return nil, err + } else { + if utils.IsAllowedNamespaces(ressourceNamespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &S3ClientNotFound{Reason: "Client \"default\" was not found"} + return nil, err + } + } + } else { + logger.Info(fmt.Sprintf("Resource doesn't refer to s3Instance: %s, search instance in cache", ressourceS3InstanceRef)) + clientName := "" + if strings.Contains(ressourceS3InstanceRef, "/") { + clientName = ressourceS3InstanceRef + } else { + clientName = ressourceNamespace + "/" + ressourceS3InstanceRef + } + s3Client, found := s3ClientCache.Get(clientName) + if !found { + err := &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 resource %s can use S3Instance %s", ressourceName, clientName)) + if utils.IsAllowedNamespaces(ressourceNamespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &S3ClientNotFound{Reason: fmt.Sprintf("Client %s is not allowed in this namespace", ressourceS3InstanceRef)} + return nil, err + } + } +} + type S3ClientCacheError struct { Reason string } +type S3ClientNotFound struct { + Reason string +} + func (r *S3ClientCacheError) Error() string { - return fmt.Sprintf("%s", r.Reason) + return r.Reason +} + +func (r *S3ClientNotFound) Error() string { + return 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..3c410a0 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "flag" "fmt" "os" + "time" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -28,8 +29,6 @@ import ( s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" 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" @@ -72,19 +71,7 @@ func main() { var probeAddr string // S3 related variables - var s3EndpointUrl string - var accessKey string - var secretKey string - var region string - var s3Provider string - var useSsl bool - var caCertificatesBase64 ArrayFlags - var caCertificatesBundlePath string - var bucketDeletion bool - var policyDeletion bool - var pathDeletion bool - var s3userDeletion bool - var s3LabelSelector string + var reconcilePeriod time.Duration //K8S related variable var overrideExistingSecret bool @@ -94,27 +81,17 @@ func main() { flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") + flag.DurationVar(&reconcilePeriod, "reconcile-period", 0, + "Default reconcile period for controllers. Zero to disable periodic reconciliation") // 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.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.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") opts := zap.Options{ Development: true, TimeEncoder: zapcore.ISO8601TimeEncoder, } + opts.BindFlags(flag.CommandLine) flag.Parse() @@ -156,67 +133,44 @@ func main() { s3ClientCache := s3ClientCache.New() - // Creation of the default S3 client - s3DefaultClient, err := factory.GenerateDefaultS3Client(s3Provider, s3EndpointUrl, accessKey, secretKey, region, useSsl, caCertificatesBase64, caCertificatesBundlePath) - - 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) + if err = (&controllers.BucketReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + S3ClientCache: s3ClientCache, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Bucket") os.Exit(1) } - - if s3DefaultClient != nil { - s3ClientCache.Set("default", s3DefaultClient) + if err = (&controllers.PathReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + S3ClientCache: s3ClientCache, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Path") + os.Exit(1) } - - if err = (&controllers.BucketReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - S3ClientCache: s3ClientCache, - BucketDeletion: bucketDeletion, - S3LabelSelectorValue: s3LabelSelector, + if err = (&controllers.PolicyReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + S3ClientCache: s3ClientCache, }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Bucket") + setupLog.Error(err, "unable to create controller", "controller", "Policy") + os.Exit(1) + } + if err = (&controllers.S3UserReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + S3ClientCache: s3ClientCache, + OverrideExistingSecret: overrideExistingSecret, + }).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(), - S3ClientCache: s3ClientCache, - S3LabelSelectorValue: s3LabelSelector, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + S3ClientCache: s3ClientCache, + ReconcilePeriod: reconcilePeriod, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "S3Instance") os.Exit(1)