From de342d8b38a3fd2ba95af01e1ffac83b93303618 Mon Sep 17 00:00:00 2001 From: Eneman Donatien Date: Fri, 13 Sep 2024 15:06:15 +0200 Subject: [PATCH] [ENH] :sparkles: implement s3instanceref and default and add allowedNamespaces --- README.md | 43 +- api/v1alpha1/bucket_types.go | 8 +- api/v1alpha1/path_types.go | 6 +- api/v1alpha1/policy_types.go | 6 +- api/v1alpha1/s3instance_types.go | 35 +- api/v1alpha1/s3user_types.go | 6 +- api/v1alpha1/types.go | 16 + api/v1alpha1/zz_generated.deepcopy.go | 4 +- config/crd/bases/s3.onyxia.sh_buckets.yaml | 8 + config/crd/bases/s3.onyxia.sh_paths.yaml | 7 + config/crd/bases/s3.onyxia.sh_policies.yaml | 7 + .../crd/bases/s3.onyxia.sh_s3instances.yaml | 57 ++- config/crd/bases/s3.onyxia.sh_s3users.yaml | 7 + config/rbac/role.yaml | 55 ++- .../s3.onyxia.sh_v1alpha1_s3instance.yaml | 32 +- controllers/bucket_controller.go | 257 ++++++----- controllers/path_controller.go | 164 +++---- controllers/policy_controller.go | 159 +++---- controllers/s3instance_controller.go | 423 ++++++++++++------ controllers/user_controller.go | 213 ++++----- go.mod | 140 +++--- go.sum | 360 +++++++-------- internal/s3/factory/interface.go | 50 +-- internal/s3/factory/minioS3Client.go | 150 ++++--- internal/s3/s3ClientCache.go | 62 ++- internal/utils/const.go | 7 - internal/utils/utils.go | 16 + main.go | 118 ++--- 28 files changed, 1356 insertions(+), 1060 deletions(-) create mode 100644 api/v1alpha1/types.go delete mode 100644 internal/utils/const.go diff --git a/README.md b/README.md index 7873585..d19c856 100644 --- a/README.md +++ b/README.md @@ -74,25 +74,12 @@ 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. | | +| `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: @@ -166,10 +153,15 @@ metadata: name: s3-default-instance # Name of the S3Instance spec: s3Provider: minio # Type of the Provider. Can be "mockedS3Provider" or "minio" - urlEndpoint: minio.example.com # URL of the Provider - secretName: minio-credentials # Name of the secret containing 2 Keys S3_ACCESS_KEY and S3_SECRET_KEY + url: https://minio.example.com # URL of the Provider + secretRef: minio-credentials # Name of the secret containing 2 Keys S3_ACCESS_KEY and S3_SECRET_KEY + caCertSecretRef: minio-certs # Name of the secret containing key ca.crt with cert of s3provider 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 + bucketDeletionEnabled: true # Allowed bucket entity suppression on s3instance + policyDeletionEnabled: true # Allowed policy entity suppression on s3instance + pathDeletionEnabled: true # Allowed path entity suppression on s3instance + s3UserDeletionEnabled: true # Allowed s3User entity suppression on s3instance ``` ### 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..71bddee 100644 --- a/api/v1alpha1/bucket_types.go +++ b/api/v1alpha1/bucket_types.go @@ -37,8 +37,12 @@ type BucketSpec struct { Paths []string `json:"paths,omitempty"` // s3InstanceRef where create the bucket - // +kubebuilder:validation:Optional - S3InstanceRef string `json:"s3InstanceRef,omitempty"` + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?(/[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?)?$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=127 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="s3InstanceRef is immutable" + // +kubebuilder:default=s3-operator/default + S3InstanceRef string `json:"s3InstanceRef"` // Quota to apply to the bucket // +kubebuilder:validation:Required diff --git a/api/v1alpha1/path_types.go b/api/v1alpha1/path_types.go index 45c7ce9..f920a6e 100644 --- a/api/v1alpha1/path_types.go +++ b/api/v1alpha1/path_types.go @@ -37,7 +37,11 @@ type PathSpec struct { Paths []string `json:"paths,omitempty"` // s3InstanceRef where create the Paths - // +kubebuilder:validation:Optional + // +kubebuilder:default=s3-operator/default + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="s3InstanceRef is immutable" + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?(/[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?)?$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=127 S3InstanceRef string `json:"s3InstanceRef,omitempty"` } diff --git a/api/v1alpha1/policy_types.go b/api/v1alpha1/policy_types.go index ac7a07c..fadde82 100644 --- a/api/v1alpha1/policy_types.go +++ b/api/v1alpha1/policy_types.go @@ -37,7 +37,11 @@ type PolicySpec struct { PolicyContent string `json:"policyContent"` // s3InstanceRef where create the Policy - // +kubebuilder:validation:Optional + // +kubebuilder:default=s3-operator/default + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="s3InstanceRef is immutable" + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?(/[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?)?$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=127 S3InstanceRef string `json:"s3InstanceRef,omitempty"` } diff --git a/api/v1alpha1/s3instance_types.go b/api/v1alpha1/s3instance_types.go index eb03eff..c57002e 100644 --- a/api/v1alpha1/s3instance_types.go +++ b/api/v1alpha1/s3instance_types.go @@ -28,27 +28,46 @@ type S3InstanceSpec struct { // type of the S3Instance // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="S3Provider is immutable" + // +kubebuilder:default=minio + // +kubebuilder:validation:Enum=minio;mockedS3Provider 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 - UseSSL bool `json:"useSSL,omitempty"` + CaCertSecretRef string `json:"caCertSecretRef,omitempty"` - // CaCertificatesBase64 associated to the S3InstanceUrl + // AllowedNamespaces to use this S3InstanceUrl if empty only the namespace of this instance url is allowed to use it // +kubebuilder:validation:Optional - CaCertificatesBase64 []string `json:"caCertificateBase64,omitempty"` + AllowedNamespaces []string `json:"allowedNamespaces,omitempty"` + + // BucketDeletionEnabled Trigger bucket deletion on the S3 backend upon CR deletion. Will fail if bucket is not empty. + // +kubebuilder:default=false + BucketDeletionEnabled bool `json:"bucketDeletionEnabled"` + + // PolicyDeletionEnabled Trigger policy deletion on the S3 backend upon CR deletion. + // +kubebuilder:default=false + PolicyDeletionEnabled bool `json:"policyDeletionEnabled"` + + // PathDeletionEnabled Trigger path deletion on the S3 backend upon CR deletion. Limited to deleting the `.keep` files used by the operator. + // +kubebuilder:default=false + PathDeletionEnabled bool `json:"pathDeletionEnabled"` + + // S3UserDeletionEnabled Trigger S3 deletion on the S3 backend upon CR deletion. + // +kubebuilder:default=false + S3UserDeletionEnabled bool `json:"s3UserDeletionEnabled"` } // S3InstanceStatus defines the observed state of S3Instance diff --git a/api/v1alpha1/s3user_types.go b/api/v1alpha1/s3user_types.go index e116a92..94f844a 100644 --- a/api/v1alpha1/s3user_types.go +++ b/api/v1alpha1/s3user_types.go @@ -39,7 +39,11 @@ type S3UserSpec struct { SecretName string `json:"secretName"` // s3InstanceRef where create the user - // +kubebuilder:validation:Optional + // +kubebuilder:default=s3-operator/default + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="s3InstanceRef is immutable" + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?(/[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?)?$` + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=127 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..1d2daf6 100644 --- a/config/crd/bases/s3.onyxia.sh_buckets.yaml +++ b/config/crd/bases/s3.onyxia.sh_buckets.yaml @@ -58,11 +58,19 @@ spec: - default type: object s3InstanceRef: + default: s3-operator/default description: s3InstanceRef where create the bucket + maxLength: 127 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?(/[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?)?$ type: string + x-kubernetes-validations: + - message: s3InstanceRef is immutable + rule: self == oldSelf required: - name - quota + - s3InstanceRef type: object status: description: BucketStatus defines the observed state of Bucket diff --git a/config/crd/bases/s3.onyxia.sh_paths.yaml b/config/crd/bases/s3.onyxia.sh_paths.yaml index c124fd0..b825e91 100644 --- a/config/crd/bases/s3.onyxia.sh_paths.yaml +++ b/config/crd/bases/s3.onyxia.sh_paths.yaml @@ -44,8 +44,15 @@ spec: type: string type: array s3InstanceRef: + default: s3-operator/default description: s3InstanceRef where create the Paths + maxLength: 127 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?(/[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?)?$ 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..9ee65c5 100644 --- a/config/crd/bases/s3.onyxia.sh_policies.yaml +++ b/config/crd/bases/s3.onyxia.sh_policies.yaml @@ -42,8 +42,15 @@ spec: description: Content of the policy (IAM JSON format) type: string s3InstanceRef: + default: s3-operator/default description: s3InstanceRef where create the Policy + maxLength: 127 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?(/[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?)?$ 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..4627360 100644 --- a/config/crd/bases/s3.onyxia.sh_s3instances.yaml +++ b/config/crd/bases/s3.onyxia.sh_s3instances.yaml @@ -35,32 +35,65 @@ 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 + bucketDeletionEnabled: + default: false + description: BucketDeletionEnabled Trigger bucket deletion on the + S3 backend upon CR deletion. Will fail if bucket is not empty. + type: boolean + caCertSecretRef: + description: Secret containing key ca.crt with the certificate associated + to the S3InstanceUrl + type: string + pathDeletionEnabled: + default: false + description: PathDeletionEnabled Trigger path deletion on the S3 backend + upon CR deletion. Limited to deleting the `.keep` files used by + the operator. + type: boolean + policyDeletionEnabled: + default: false + description: PolicyDeletionEnabled Trigger policy deletion on the + S3 backend upon CR deletion. + type: boolean region: description: region associated to the S3Instance type: string s3Provider: + default: minio description: type of the S3Instance + enum: + - minio + - mockedS3Provider type: string - secretName: - description: SecretName associated to the S3Instance containing accessKey - and secretKey + x-kubernetes-validations: + - message: S3Provider is immutable + rule: self == oldSelf + s3UserDeletionEnabled: + default: false + description: S3UserDeletionEnabled Trigger S3 deletion on the S3 backend + upon CR deletion. + type: boolean + 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 + - bucketDeletionEnabled + - pathDeletionEnabled + - policyDeletionEnabled - s3Provider - - secretName - - urlEndpoint + - s3UserDeletionEnabled + - 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..5ed8813 100644 --- a/config/crd/bases/s3.onyxia.sh_s3users.yaml +++ b/config/crd/bases/s3.onyxia.sh_s3users.yaml @@ -44,8 +44,15 @@ spec: type: string type: array s3InstanceRef: + default: s3-operator/default description: s3InstanceRef where create the user + maxLength: 127 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?(/[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?)?$ 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/rbac/role.yaml b/config/rbac/role.yaml index 559db62..fc25de9 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -5,10 +5,35 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - update + - watch +- apiGroups: + - "" + resources: + - secrets/finalizers + verbs: + - update +- apiGroups: + - "" + resources: + - secrets/status + verbs: + - get + - patch + - update - apiGroups: - s3.onyxia.sh resources: - - S3Instance + - buckets verbs: - create - delete @@ -20,13 +45,13 @@ rules: - apiGroups: - s3.onyxia.sh resources: - - S3Instance/finalizers + - buckets/finalizers verbs: - update - apiGroups: - s3.onyxia.sh resources: - - S3Instance/status + - buckets/status verbs: - get - patch @@ -34,7 +59,7 @@ rules: - apiGroups: - s3.onyxia.sh resources: - - S3User + - paths verbs: - create - delete @@ -46,13 +71,13 @@ rules: - apiGroups: - s3.onyxia.sh resources: - - S3User/finalizers + - paths/finalizers verbs: - update - apiGroups: - s3.onyxia.sh resources: - - S3User/status + - paths/status verbs: - get - patch @@ -60,7 +85,7 @@ rules: - apiGroups: - s3.onyxia.sh resources: - - buckets + - policies verbs: - create - delete @@ -72,13 +97,13 @@ rules: - apiGroups: - s3.onyxia.sh resources: - - buckets/finalizers + - policies/finalizers verbs: - update - apiGroups: - s3.onyxia.sh resources: - - buckets/status + - policies/status verbs: - get - patch @@ -86,7 +111,7 @@ rules: - apiGroups: - s3.onyxia.sh resources: - - paths + - s3instances verbs: - create - delete @@ -98,13 +123,13 @@ rules: - apiGroups: - s3.onyxia.sh resources: - - paths/finalizers + - s3instances/finalizers verbs: - update - apiGroups: - s3.onyxia.sh resources: - - paths/status + - s3instances/status verbs: - get - patch @@ -112,7 +137,7 @@ rules: - apiGroups: - s3.onyxia.sh resources: - - policies + - s3users verbs: - create - delete @@ -124,13 +149,13 @@ rules: - apiGroups: - s3.onyxia.sh resources: - - policies/finalizers + - s3users/finalizers verbs: - update - apiGroups: - s3.onyxia.sh resources: - - policies/status + - s3users/status verbs: - get - patch diff --git a/config/samples/s3.onyxia.sh_v1alpha1_s3instance.yaml b/config/samples/s3.onyxia.sh_v1alpha1_s3instance.yaml index 198da17..4b90d29 100644 --- a/config/samples/s3.onyxia.sh_v1alpha1_s3instance.yaml +++ b/config/samples/s3.onyxia.sh_v1alpha1_s3instance.yaml @@ -2,15 +2,33 @@ 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: minio-credentials +type: Opaque +data: + S3_ACCESS_KEY: accessKey + S3_SECRET_KEY: secretkey +--- +apiVersion: v1 +kind: Secret +metadata: + name: s3-default-instance-cert +type: Opaque +data: + ca.crt: LS...... diff --git a/controllers/bucket_controller.go b/controllers/bucket_controller.go index 5e88562..2aab231 100644 --- a/controllers/bucket_controller.go +++ b/controllers/bucket_controller.go @@ -26,7 +26,7 @@ import ( "github.com/InseeFrLab/s3-operator/internal/s3/factory" utils "github.com/InseeFrLab/s3-operator/internal/utils" - "k8s.io/apimachinery/pkg/api/errors" + k8sapierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilerrors "k8s.io/apimachinery/pkg/util/errors" @@ -37,23 +37,23 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // 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 + firstRun bool } +const bucketFinalizer = "s3.onyxia.sh/finalizer" + //+kubebuilder:rbac:groups=s3.onyxia.sh,resources=buckets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=s3.onyxia.sh,resources=buckets/status,verbs=get;update;patch //+kubebuilder:rbac:groups=s3.onyxia.sh,resources=buckets/finalizers,verbs=update -const bucketFinalizer = "s3.onyxia.sh/finalizer" - // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // @@ -62,11 +62,17 @@ const bucketFinalizer = "s3.onyxia.sh/finalizer" func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) + // wait s3instance in cache + if !r.firstRun { + r.firstRun = true + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + // Checking for bucket resource existence bucketResource := &s3v1alpha1.Bucket{} err := r.Get(ctx, req.NamespacedName, bucketResource) if err != nil { - if errors.IsNotFound(err) { + if k8sapierrors.IsNotFound(err) { logger.Info("The Bucket custom resource has been removed ; as such the Bucket controller is NOOP.", "req.Name", req.Name) return ctrl.Result{}, nil } @@ -74,51 +80,9 @@ 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 - if isMarkedForDeletion { - if controllerutil.ContainsFinalizer(bucketResource, bucketFinalizer) { - // Run finalization logic for bucketFinalizer. If the - // finalization logic fails, don't remove the finalizer so - // that we can retry during the next reconciliation. - if err := r.finalizeBucket(ctx, bucketResource); err != nil { - // return ctrl.Result{}, err - logger.Error(err, "an error occurred when attempting to finalize the bucket", "bucket", bucketResource.Spec.Name) - // return ctrl.Result{}, err - return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "BucketFinalizeFailed", - fmt.Sprintf("An error occurred when attempting to delete bucket [%s]", bucketResource.Spec.Name), err) - } - - // Remove bucketFinalizer. Once all finalizers have been - // removed, the object will be deleted. - controllerutil.RemoveFinalizer(bucketResource, bucketFinalizer) - err := r.Update(ctx, bucketResource) - if err != nil { - logger.Error(err, "an error occurred when removing finalizer from bucket", "bucket", bucketResource.Spec.Name) - // return ctrl.Result{}, err - return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "BucketFinalizerRemovalFailed", - fmt.Sprintf("An error occurred when attempting to remove the finalizer from bucket [%s]", bucketResource.Spec.Name), err) - } - } - return ctrl.Result{}, nil - } - // Add finalizer for this CR if !controllerutil.ContainsFinalizer(bucketResource, bucketFinalizer) { + logger.Info("Adding finalizer on ressource") controllerutil.AddFinalizer(bucketResource, bucketFinalizer) err = r.Update(ctx, bucketResource) if err != nil { @@ -126,14 +90,44 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "BucketFinalizerAddFailed", fmt.Sprintf("An error occurred when attempting to add the finalizer from bucket [%s]", bucketResource.Spec.Name), 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, bucketResource); err != nil { + logger.Error(err, "Failed to re-fetch bucketResource", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, err + } + } + + // // Managing bucket deletion with a finalizer + // // REF : https://sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#external-resources + if bucketResource.GetDeletionTimestamp() != nil { + logger.Info("bucketResource have been marked for deletion") + return r.handleDeletion(ctx, req, bucketResource) } - // Create S3Client - s3Client, err := r.getS3InstanceForObject(ctx, bucketResource) + return r.handleReconciliation(ctx, bucketResource) + +} + +func (r *BucketReconciler) handleReconciliation(ctx context.Context, bucketResource *s3v1alpha1.Bucket) (reconcile.Result, error) { + logger := log.FromContext(ctx) + logger.Info("Je lance cette version commentée") + + 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 @@ -147,36 +141,28 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // If the bucket does not exist, it is created based on the CR (with potential quotas and paths) if !found { + return r.handleBucketCreation(ctx, bucketResource) + } - // Bucket creation - err = s3Client.CreateBucket(bucketResource.Spec.Name) - if err != nil { - logger.Error(err, "an error occurred while creating a bucket", "bucket", bucketResource.Spec.Name) - return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "BucketCreationFailed", - fmt.Sprintf("Creation of bucket [%s] on S3 instance has failed", bucketResource.Spec.Name), err) - } + logger.Info("this bucket already exists and will be reconciled") + return r.handleBucketUpdate(ctx, bucketResource) - // Setting quotas - err = s3Client.SetQuota(bucketResource.Spec.Name, bucketResource.Spec.Quota.Default) - if err != nil { - logger.Error(err, "an error occurred while setting a quota on a bucket", "bucket", bucketResource.Spec.Name, "quota", bucketResource.Spec.Quota.Default) - return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "SetQuotaOnBucketFailed", - fmt.Sprintf("Setting a quota of [%v] on bucket [%s] has failed", bucketResource.Spec.Quota.Default, bucketResource.Spec.Name), err) - } +} - // Path creation - for _, v := range bucketResource.Spec.Paths { - err = s3Client.CreatePath(bucketResource.Spec.Name, v) - if err != nil { - logger.Error(err, "an error occurred while creating a path on a bucket", "bucket", bucketResource.Spec.Name, "path", v) - return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "CreatingPathOnBucketFailed", - fmt.Sprintf("Creating the path [%s] on bucket [%s] has failed", v, bucketResource.Spec.Name), err) - } - } +func (r *BucketReconciler) handleBucketUpdate(ctx context.Context, bucketResource *s3v1alpha1.Bucket) (reconcile.Result, error) { + logger := log.FromContext(ctx) - // The bucket creation, quota setting and path creation happened without any error - return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorSucceeded", metav1.ConditionTrue, "BucketCreated", - fmt.Sprintf("The bucket [%s] was created with its quota and paths", bucketResource.Spec.Name), nil) + s3Client, err := r.getS3InstanceForObject(bucketResource) + if err != nil { + 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) + } } // If the bucket exists on the S3 server, then we need to compare it to @@ -236,7 +222,86 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // The bucket reconciliation with its CR was succesful (or NOOP) return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorSucceeded", metav1.ConditionTrue, "BucketUpdated", fmt.Sprintf("The bucket [%s] was updated according to its matching custom resource", bucketResource.Spec.Name), nil) +} + +func (r *BucketReconciler) handleBucketCreation(ctx context.Context, bucketResource *s3v1alpha1.Bucket) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + s3Client, err := r.getS3InstanceForObject(bucketResource) + if err != nil { + 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 creation + err = s3Client.CreateBucket(bucketResource.Spec.Name) + if err != nil { + logger.Error(err, "an error occurred while creating a bucket", "bucket", bucketResource.Spec.Name) + return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "BucketCreationFailed", + fmt.Sprintf("Creation of bucket [%s] on S3 instance has failed", bucketResource.Spec.Name), err) + } + + // Setting quotas + err = s3Client.SetQuota(bucketResource.Spec.Name, bucketResource.Spec.Quota.Default) + if err != nil { + logger.Error(err, "an error occurred while setting a quota on a bucket", "bucket", bucketResource.Spec.Name, "quota", bucketResource.Spec.Quota.Default) + return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "SetQuotaOnBucketFailed", + fmt.Sprintf("Setting a quota of [%v] on bucket [%s] has failed", bucketResource.Spec.Quota.Default, bucketResource.Spec.Name), err) + } + + // Path creation + for _, v := range bucketResource.Spec.Paths { + err = s3Client.CreatePath(bucketResource.Spec.Name, v) + if err != nil { + logger.Error(err, "an error occurred while creating a path on a bucket", "bucket", bucketResource.Spec.Name, "path", v) + return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "CreatingPathOnBucketFailed", + fmt.Sprintf("Creating the path [%s] on bucket [%s] has failed", v, bucketResource.Spec.Name), err) + } + } + // The bucket creation, quota setting and path creation happened without any error + return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorSucceeded", metav1.ConditionTrue, "BucketCreated", + fmt.Sprintf("The bucket [%s] was created with its quota and paths", bucketResource.Spec.Name), nil) +} + +func (r *BucketReconciler) handleDeletion(ctx context.Context, req reconcile.Request, bucketResource *s3v1alpha1.Bucket) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + if controllerutil.ContainsFinalizer(bucketResource, bucketFinalizer) { + logger.Info("Je suis ici") + + if err := r.finalizeBucket(ctx, bucketResource); err != nil { + + logger.Error(err, "an error occurred when attempting to finalize the bucket", "bucket", bucketResource.Spec.Name) + + return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "BucketFinalizeFailed", + fmt.Sprintf("An error occurred when attempting to delete bucket [%s]", bucketResource.Spec.Name), err) + } + + if ok := controllerutil.RemoveFinalizer(bucketResource, bucketFinalizer); !ok { + logger.Info("Failed to remove finalizer for bucketResource", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{Requeue: true}, nil + } + + // Let's re-fetch the S3Instance Custom Resource after removing 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.Update(ctx, bucketResource); err != nil { + logger.Error(err, "Failed to remove finalizer for bucketResource", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, err + } + + } + return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager.* @@ -261,12 +326,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 +360,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..186e174 100644 --- a/controllers/path_controller.go +++ b/controllers/path_controller.go @@ -22,7 +22,7 @@ import ( "time" s3ClientCache "github.com/InseeFrLab/s3-operator/internal/s3" - "k8s.io/apimachinery/pkg/api/errors" + k8sapierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilerrors "k8s.io/apimachinery/pkg/util/errors" @@ -33,6 +33,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" "github.com/InseeFrLab/s3-operator/internal/s3/factory" @@ -42,10 +43,9 @@ 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 + firstRun bool } //+kubebuilder:rbac:groups=s3.onyxia.sh,resources=paths,verbs=get;list;watch;create;update;patch;delete @@ -62,11 +62,17 @@ const pathFinalizer = "s3.onyxia.sh/finalizer" func (r *PathReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) + // wait s3instance in cache + if !r.firstRun { + r.firstRun = true + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + // Checking for path resource existence pathResource := &s3v1alpha1.Path{} err := r.Get(ctx, req.NamespacedName, pathResource) if err != nil { - if errors.IsNotFound(err) { + if k8sapierrors.IsNotFound(err) { logger.Info("The Path custom resource has been removed ; as such the Path controller is NOOP.", "req.Name", req.Name) return ctrl.Result{}, nil } @@ -74,49 +80,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 - if isMarkedForDeletion { - if controllerutil.ContainsFinalizer(pathResource, pathFinalizer) { - // Run finalization logic for pathFinalizer. If the - // finalization logic fails, don't remove the finalizer so - // that we can retry during the next reconciliation. - if err := r.finalizePath(ctx, pathResource); err != nil { - // return ctrl.Result{}, err - logger.Error(err, "an error occurred when attempting to finalize the path", "path", pathResource.Name) - // return ctrl.Result{}, err - return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorFailed", metav1.ConditionFalse, "PathFinalizeFailed", - fmt.Sprintf("An error occurred when attempting to delete path [%s]", pathResource.Name), err) - } - - // Remove pathFinalizer. Once all finalizers have been - // removed, the object will be deleted. - controllerutil.RemoveFinalizer(pathResource, pathFinalizer) - err := r.Update(ctx, pathResource) - if err != nil { - logger.Error(err, "an error occurred when removing finalizer from path", "path", pathResource.Name) - // return ctrl.Result{}, err - return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorFailed", metav1.ConditionFalse, "PathFinalizerRemovalFailed", - fmt.Sprintf("An error occurred when attempting to remove the finalizer from path [%s]", pathResource.Name), err) - } - } - return ctrl.Result{}, nil - } - // Add finalizer for this CR if !controllerutil.ContainsFinalizer(pathResource, pathFinalizer) { controllerutil.AddFinalizer(pathResource, pathFinalizer) @@ -127,14 +90,43 @@ func (r *PathReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorFailed", metav1.ConditionFalse, "PathFinalizerAddFailed", fmt.Sprintf("An error occurred when attempting to add the finalizer from path [%s]", pathResource.Name), 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, pathResource); err != nil { + logger.Error(err, "Failed to re-fetch pathResource", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, err + } } + // Managing path deletion with a finalizer + // REF : https://sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#external-resources + if pathResource.GetDeletionTimestamp() != nil { + return r.handlePathDeletion(ctx, req, pathResource) + } + + return r.handlePathReconciliation(ctx, pathResource) + +} + +func (r *PathReconciler) handlePathReconciliation(ctx context.Context, pathResource *s3v1alpha1.Path) (reconcile.Result, error) { + + logger := log.FromContext(ctx) + // 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 @@ -184,7 +176,45 @@ func (r *PathReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. // The bucket reconciliation with its CR was succesful (or NOOP) return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorSucceeded", metav1.ConditionTrue, "PathsCreated", fmt.Sprintf("The paths were created according to the specs of the [%s] CR", pathResource.Name), nil) +} + +func (r *PathReconciler) handlePathDeletion(ctx context.Context, req reconcile.Request, pathResource *s3v1alpha1.Path) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + if controllerutil.ContainsFinalizer(pathResource, pathFinalizer) { + // Run finalization logic for pathFinalizer. If the + // finalization logic fails, don't remove the finalizer so + // that we can retry during the next reconciliation. + if err := r.finalizePath(ctx, pathResource); err != nil { + // return ctrl.Result{}, err + logger.Error(err, "an error occurred when attempting to finalize the path", "path", pathResource.Name) + // return ctrl.Result{}, err + return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorFailed", metav1.ConditionFalse, "PathFinalizeFailed", + fmt.Sprintf("An error occurred when attempting to delete path [%s]", pathResource.Name), err) + } + + // Remove pathFinalizer. Once all finalizers have been + // removed, the object will be deleted. + + if ok := controllerutil.RemoveFinalizer(pathResource, pathFinalizer); !ok { + logger.Info("Failed to remove finalizer for S3Instance", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{Requeue: true}, nil + } + + // Let's re-fetch the S3Instance Custom Resource after removing 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.Update(ctx, pathResource); err != nil { + logger.Error(err, "an error occurred when removing finalizer from path", "path", pathResource.Name) + // return ctrl.Result{}, err + return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorFailed", metav1.ConditionFalse, "PathFinalizerRemovalFailed", + fmt.Sprintf("An error occurred when attempting to remove the finalizer from path [%s]", pathResource.Name), err) + } + } + return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. @@ -208,13 +238,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 +291,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..5752bb2 100644 --- a/controllers/policy_controller.go +++ b/controllers/policy_controller.go @@ -24,7 +24,7 @@ import ( "time" "github.com/minio/madmin-go/v3" - "k8s.io/apimachinery/pkg/api/errors" + k8sapierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" utilerrors "k8s.io/apimachinery/pkg/util/errors" @@ -35,6 +35,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" s3ClientCache "github.com/InseeFrLab/s3-operator/internal/s3" @@ -45,10 +46,9 @@ 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 + firstRun bool } //+kubebuilder:rbac:groups=s3.onyxia.sh,resources=policies,verbs=get;list;watch;create;update;patch;delete @@ -65,11 +65,17 @@ const policyFinalizer = "s3.onyxia.sh/finalizer" func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) + // wait s3instance in cache + if !r.firstRun { + r.firstRun = true + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + // Checking for policy resource existence policyResource := &s3v1alpha1.Policy{} err := r.Get(ctx, req.NamespacedName, policyResource) if err != nil { - if errors.IsNotFound(err) { + if k8sapierrors.IsNotFound(err) { logger.Info("The Policy custom resource has been removed ; as such the Policy controller is NOOP.", "req.Name", req.Name) return ctrl.Result{}, nil } @@ -77,49 +83,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 - if isMarkedForDeletion { - if controllerutil.ContainsFinalizer(policyResource, policyFinalizer) { - // Run finalization logic for policyFinalizer. If the - // finalization logic fails, don't remove the finalizer so - // that we can retry during the next reconciliation. - if err := r.finalizePolicy(ctx, policyResource); err != nil { - // return ctrl.Result{}, err - logger.Error(err, "an error occurred when attempting to finalize the policy", "policy", policyResource.Spec.Name) - // return ctrl.Result{}, err - return r.SetPolicyStatusConditionAndUpdate(ctx, policyResource, "OperatorFailed", metav1.ConditionFalse, "PolicyFinalizeFailed", - fmt.Sprintf("An error occurred when attempting to delete policy [%s]", policyResource.Spec.Name), err) - } - - // Remove policyFinalizer. Once all finalizers have been - // removed, the object will be deleted. - controllerutil.RemoveFinalizer(policyResource, policyFinalizer) - err := r.Update(ctx, policyResource) - if err != nil { - logger.Error(err, "an error occurred when removing finalizer from policy", "policy", policyResource.Spec.Name) - // return ctrl.Result{}, err - return r.SetPolicyStatusConditionAndUpdate(ctx, policyResource, "OperatorFailed", metav1.ConditionFalse, "PolicyFinalizerRemovalFailed", - fmt.Sprintf("An error occurred when attempting to remove the finalizer from policy [%s]", policyResource.Spec.Name), err) - } - } - return ctrl.Result{}, nil - } - // Add finalizer for this CR if !controllerutil.ContainsFinalizer(policyResource, policyFinalizer) { controllerutil.AddFinalizer(policyResource, policyFinalizer) @@ -130,16 +93,45 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return r.SetPolicyStatusConditionAndUpdate(ctx, policyResource, "OperatorFailed", metav1.ConditionFalse, "PolicyFinalizerAddFailed", fmt.Sprintf("An error occurred when attempting to add the finalizer from policy [%s]", policyResource.Spec.Name), 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, policyResource); err != nil { + logger.Error(err, "Failed to re-fetch policyResource", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, err + } + } + + // Managing policy deletion with a finalizer + // REF : https://sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#external-resources + if policyResource.GetDeletionTimestamp() != nil { + + return r.handlePolicyDeletion(ctx, req, policyResource) } // Policy lifecycle management (other than deletion) starts here + return r.handlePolicyReconciliation(ctx, policyResource) + +} + +func (r *PolicyReconciler) handlePolicyReconciliation(ctx context.Context, policyResource *s3v1alpha1.Policy) (reconcile.Result, error) { + logger := log.FromContext(ctx) // 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 @@ -195,6 +187,40 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr fmt.Sprintf("The policy [%s] was updated according to its matching custom resource", policyResource.Spec.Name), nil) } +func (r *PolicyReconciler) handlePolicyDeletion(ctx context.Context, req reconcile.Request, policyResource *s3v1alpha1.Policy) (reconcile.Result, error) { + logger := log.FromContext(ctx) + if controllerutil.ContainsFinalizer(policyResource, policyFinalizer) { + // Run finalization logic for policyFinalizer. If the + // finalization logic fails, don't remove the finalizer so + // that we can retry during the next reconciliation. + if err := r.finalizePolicy(ctx, policyResource); err != nil { + // return ctrl.Result{}, err + logger.Error(err, "an error occurred when attempting to finalize the policy", "policy", policyResource.Spec.Name) + // return ctrl.Result{}, err + return r.SetPolicyStatusConditionAndUpdate(ctx, policyResource, "OperatorFailed", metav1.ConditionFalse, "PolicyFinalizeFailed", + fmt.Sprintf("An error occurred when attempting to delete policy [%s]", policyResource.Spec.Name), err) + } + + // Remove policyFinalizer. Once all finalizers have been + // removed, the object will be deleted. + controllerutil.RemoveFinalizer(policyResource, policyFinalizer) + + if ok := controllerutil.RemoveFinalizer(policyResource, policyFinalizer); !ok { + logger.Info("Failed to remove finalizer for S3Instance", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{Requeue: true}, nil + } + + err := r.Update(ctx, policyResource) + if err != nil { + logger.Error(err, "an error occurred when removing finalizer from policy", "policy", policyResource.Spec.Name) + // return ctrl.Result{}, err + return r.SetPolicyStatusConditionAndUpdate(ctx, policyResource, "OperatorFailed", metav1.ConditionFalse, "PolicyFinalizerRemovalFailed", + fmt.Sprintf("An error occurred when attempting to remove the finalizer from policy [%s]", policyResource.Spec.Name), err) + } + } + return ctrl.Result{}, nil +} + // SetupWithManager sets up the controller with the Manager. func (r *PolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). @@ -235,12 +261,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 +295,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..728b19b 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,15 +48,16 @@ type S3InstanceReconciler struct { Scheme *runtime.Scheme S3ClientCache *s3ClientCache.S3ClientCache S3LabelSelectorValue string + ReconcilePeriod time.Duration } const ( - s3InstanceFinalizer = "s3.onyxia.sh/s3InstanceFinalizer" + s3InstanceFinalizer = "s3.onyxia.sh/finalizer" ) -//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=S3Instance,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=S3Instance/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=S3Instance/finalizers,verbs=update +//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=s3instances,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=s3instances/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=s3instances/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -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 s3InstanceResource 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 s3Instance", "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 len(s3InstanceResource.Spec.AllowedNamespaces) != 0 { + 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) } - s3Config := &s3Factory.S3Config{S3Provider: s3InstanceResource.Spec.S3Provider, AccessKey: string(s3InstanceSecretSecret.Data["S3_ACCESS_KEY"]), SecretKey: string(s3InstanceSecretSecret.Data["S3_SECRET_KEY"]), S3UrlEndpoint: s3InstanceResource.Spec.UrlEndpoint, Region: s3InstanceResource.Spec.Region, UseSsl: s3InstanceResource.Spec.UseSSL, CaCertificatesBase64: s3InstanceResource.Spec.CaCertificatesBase64} + s3InstanceCaCertSecret, err := r.getS3InstanceCaCertSecret(ctx, s3InstanceResource) + if err != nil { + logger.Error(err, "Could not get 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 + } + 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) - // 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 ok := controllerutil.RemoveFinalizer(s3InstanceResource, s3InstanceFinalizer); !ok { + logger.Info("Failed to remove finalizer for S3Instance", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{Requeue: true}, nil + } + + // Let's re-fetch the S3Instance Custom Resource after removing 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.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,166 @@ 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) { +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 +} + +func (r *S3InstanceReconciler) getS3InstanceCaCertSecret(ctx context.Context, s3InstanceResource *s3v1alpha1.S3Instance) (corev1.Secret, error) { logger := log.FromContext(ctx) - secretsList := &corev1.SecretList{} - s3InstanceSecret := corev1.Secret{} + s3InstanceCaCertSecret := &corev1.Secret{} - err := r.List(ctx, secretsList, client.InNamespace(s3InstanceResource.Namespace)) + if s3InstanceResource.Spec.CaCertSecretRef == "" { + logger.Info("No CaCertSecretRef for s3instance %s", s3InstanceResource.Name) + return *s3InstanceCaCertSecret, nil + } + + err := r.Get(ctx, types.NamespacedName{Namespace: s3InstanceResource.Namespace, Name: s3InstanceResource.Spec.CaCertSecretRef}, 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) { + logger.Info("No Secret %s for s3instance %s", s3InstanceResource.Spec.CaCertSecretRef, s3InstanceResource.Name) + return *s3InstanceCaCertSecret, fmt.Errorf("secret %s not found in namespace %s", s3InstanceResource.Spec.CaCertSecretRef, 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 s3ClientCache.GetS3InstanceRefName(bucket.Spec.S3InstanceRef, bucket.Namespace) == 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 s3ClientCache.GetS3InstanceRefName(policy.Spec.S3InstanceRef, policy.Namespace) == 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 _, s3User := range s3UserList.Items { + if s3ClientCache.GetS3InstanceRefName(s3User.Spec.S3InstanceRef, s3User.Namespace) == 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 _, path := range pathList.Items { + if s3ClientCache.GetS3InstanceRefName(path.Spec.S3InstanceRef, path.Namespace) == 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..3b35ad3 100644 --- a/controllers/user_controller.go +++ b/controllers/user_controller.go @@ -24,7 +24,7 @@ import ( "time" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" + k8sapierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -50,18 +50,20 @@ type S3UserReconciler struct { client.Client Scheme *runtime.Scheme S3ClientCache *s3ClientCache.S3ClientCache - S3UserDeletion bool OverrideExistingSecret bool - S3LabelSelectorValue string + firstRun bool } const ( - userFinalizer = "s3.onyxia.sh/userFinalizer" + userFinalizer = "s3.onyxia.sh/finalizer" ) -//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=S3User,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=S3User/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=s3.onyxia.sh,resources=S3User/finalizers,verbs=update +// +kubebuilder:rbac:groups=s3.onyxia.sh,resources=s3users,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=s3.onyxia.sh,resources=s3users/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=s3.onyxia.sh,resources=s3users/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete +// +kubebuilder:rbac:groups="",resources=secrets/status,verbs=get;update;patch +// +kubebuilder:rbac:groups="",resources=secrets/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -71,11 +73,17 @@ const ( func (r *S3UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := log.FromContext(ctx) + // wait s3instance in cache + if !r.firstRun { + r.firstRun = true + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil + } + // Checking for userResource existence userResource := &s3v1alpha1.S3User{} err := r.Get(ctx, req.NamespacedName, userResource) if err != nil { - if errors.IsNotFound(err) { + if k8sapierrors.IsNotFound(err) { logger.Info(fmt.Sprintf("The S3User CR %s (or its owned Secret) has been removed. NOOP", req.Name)) return ctrl.Result{}, nil } @@ -83,26 +91,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 { - logger.Info("userResource have been marked for deletion") - return r.handleS3UserDeletion(ctx, userResource) - } - // Add finalizer for this CR if !controllerutil.ContainsFinalizer(userResource, userFinalizer) { logger.Info("adding finalizer to user") @@ -114,16 +102,44 @@ func (r *S3UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "S3UserFinalizerAddFailed", fmt.Sprintf("An error occurred when attempting to add the finalizer from user %s", userResource.Name), 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, userResource); err != nil { + logger.Error(err, "Failed to re-fetch userResource", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, err + } } - // Check user existence on the S3 server + // 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 { + logger.Info("userResource have been marked for deletion") + return r.handleS3UserDeletion(ctx, userResource) + } // Create S3Client - s3Client, err := r.getS3InstanceForObject(ctx, userResource) + // If the user does not exist, it is created based on the CR + return r.handleReconciliation(ctx, userResource) + +} + +func (r *S3UserReconciler) handleReconciliation(ctx context.Context, userResource *s3v1alpha1.S3User) (reconcile.Result, error) { + logger := log.FromContext(ctx) + 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) @@ -133,26 +149,28 @@ func (r *S3UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr fmt.Sprintf("The check for user %s's existence on the S3 backend has failed", userResource.Name), err) } - // If the user does not exist, it is created based on the CR if !found { - logger.Info("this user doesn't exist on the S3 backend and will be created", "accessKey", userResource.Spec.AccessKey) - return r.handleS3NewUser(ctx, userResource) + return r.handleS3UserCreate(ctx, userResource) } - logger.Info("this user already exists on the S3 backend and will be reconciled", "accessKey", userResource.Spec.AccessKey) - return r.handleS3ExistingUser(ctx, userResource) - + return r.handleS3UserUpdate(ctx, userResource) } -func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResource *s3v1alpha1.S3User) (reconcile.Result, error) { +func (r *S3UserReconciler) handleS3UserUpdate(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 { @@ -166,7 +184,7 @@ func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResourc return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "S3UserDeletionFailed", fmt.Sprintf("Deletion of S3user %s on S3 server has failed", userResource.Name), err) } - return r.handleS3NewUser(ctx, userResource) + return r.handleS3UserCreate(ctx, userResource) } else if err.Error() == "S3UserSecretNameMismatch" { logger.Info("A secret with owner reference to the user was found, but its name doesn't match the spec. This is probably due to the S3User's spec changing (specifically spec.secretName being added, changed or removed). The \"old\" secret will be deleted.") r.deleteSecret(ctx, &userOwnedSecret) @@ -181,7 +199,7 @@ func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResourc return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "S3UserDeletionFailed", fmt.Sprintf("Deletion of S3User %s on S3 server has failed", userResource.Name), err) } - return r.handleS3NewUser(ctx, userResource) + return r.handleS3UserCreate(ctx, userResource) } // If a matching secret is found, then we check if it is still valid, as in : do the credentials it @@ -204,7 +222,7 @@ func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResourc fmt.Sprintf("Deletion of S3user %s on S3 server has failed", userResource.Name), err) } - return r.handleS3NewUser(ctx, userResource) + return r.handleS3UserCreate(ctx, userResource) } @@ -268,15 +286,23 @@ func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResourc fmt.Sprintf("The user %s was updated according to its matching custom resource", userResource.Name), nil) } -func (r *S3UserReconciler) handleS3NewUser(ctx context.Context, userResource *s3v1alpha1.S3User) (reconcile.Result, error) { +func (r *S3UserReconciler) handleS3UserCreate(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 { @@ -300,7 +326,7 @@ func (r *S3UserReconciler) handleS3NewUser(ctx context.Context, userResource *s3 err = r.Get(ctx, types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, existingK8sSecret) // If none exist : we create the user, then the secret - if err != nil && errors.IsNotFound(err) { + if err != nil && k8sapierrors.IsNotFound(err) { logger.Info("No secret found ; creating a new Secret", "Secret.Namespace", secret.Namespace, "Secret.Name", secret.Name) // Creating the user @@ -395,9 +421,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 @@ -439,47 +464,22 @@ func (r *S3UserReconciler) handleS3UserDeletion(ctx context.Context, userResourc } func (r *S3UserReconciler) getUserSecret(ctx context.Context, userResource *s3v1alpha1.S3User) (corev1.Secret, error) { - logger := log.FromContext(ctx) - - // Listing every secrets in the S3User's namespace, as a first step - // to get the actual secret matching the S3User proper. - // TODO : proper label matching ? - secretsList := &corev1.SecretList{} - userSecret := corev1.Secret{} - - err := r.List(ctx, secretsList, client.InNamespace(userResource.Namespace)) + userSecret := &corev1.Secret{} + err := r.Get(ctx, types.NamespacedName{Namespace: userResource.Namespace, Name: userResource.Spec.SecretName}, userSecret) if err != nil { - logger.Error(err, "An error occurred while listing the secrets in user's namespace") - return userSecret, fmt.Errorf("SecretListingFailed") - } - - if len(secretsList.Items) == 0 { - logger.Info("The user's namespace doesn't appear to contain any secret") - return userSecret, nil - } - // In all the secrets inside the S3User's namespace, one should have an owner reference - // pointing to the S3User. For that specific secret, we check if its name matches the one from - // the S3User, whether explicit (userResource.Spec.SecretName) or implicit (userResource.Name) - // In case of mismatch, that secret is deleted (and will be recreated) ; if there is a match, - // it will be used for state comparison. - uid := userResource.GetUID() - - // cmp.Or takes the first non "zero" value, see https://pkg.go.dev/cmp#Or - effectiveS3UserSecretName := cmp.Or(userResource.Spec.SecretName, userResource.Name) - for _, secret := range secretsList.Items { - for _, ref := range secret.OwnerReferences { - if ref.UID == uid { - if secret.Name != effectiveS3UserSecretName { - return secret, fmt.Errorf("S3UserSecretNameMismatch") - } else { - userSecret = secret - break - } - } + if k8sapierrors.IsNotFound(err) { + return *userSecret, fmt.Errorf("secret %s not found in namespace %s", userResource.Spec.SecretName, userResource.Namespace) } + return *userSecret, err } - return userSecret, nil + for _, ref := range userSecret.OwnerReferences { + if ref.UID == userResource.GetUID() { + return *userSecret, nil + } + } + + return *userSecret, err } func (r *S3UserReconciler) deleteSecret(ctx context.Context, secret *corev1.Secret) { @@ -557,12 +557,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 +607,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..6735d47 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) + s3Logger.Info("", "hostname", hostname, "isSSL", isSSL) 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 } - // Getting the custom root CA (if any) from the "regular" client's Transport - adminClient.SetCustomTransport(minioOptions.Transport) - return &MinioS3Client{*S3Config, *minioClient, *adminClient} -} + 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 + } -func addTransportOptions(S3Config *S3Config, minioOptions *minio.Options) { - if len(S3Config.CaCertificatesBase64) > 0 { + minioOptions := &minio.Options{ + Region: region, + Secure: isSSL, + } - rootCAs, _ := x509.SystemCertPool() - if rootCAs == nil { - rootCAs = x509.NewCertPool() - } + if len(caCertificateBase64) > 0 { + addTlsClientConfigToMinioOptions(caCertificateBase64, minioOptions) + } - 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") - } + minioAdminClient.SetCustomTransport(minioOptions.Transport) - rootCAs.AppendCertsFromPEM(decodedCaCertificate) - } + return minioAdminClient, nil +} - minioOptions.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: rootCAs, - }, - } - } else if len(S3Config.CaBundlePath) > 0 { +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 +} - rootCAs, _ := x509.SystemCertPool() - if rootCAs == nil { - rootCAs = x509.NewCertPool() - } +func addTlsClientConfigToMinioOptions(caCertificatesBase64 []string, minioOptions *minio.Options) { + rootCAs, _ := x509.SystemCertPool() + 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..23e254f 100644 --- a/internal/s3/s3ClientCache.go +++ b/internal/s3/s3ClientCache.go @@ -2,9 +2,17 @@ package S3ClientCache import ( "fmt" + "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 +23,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 +33,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 +42,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 +52,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 +72,57 @@ 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) { + logger.Info(fmt.Sprintf("Resource refer to s3Instance: %s, search instance in cache", ressourceS3InstanceRef)) + s3InstanceRef := GetS3InstanceRefName(ressourceS3InstanceRef, ressourceNamespace) + s3Client, found := s3ClientCache.Get(s3InstanceRef) + if !found { + err := &S3ClientNotFound{Reason: fmt.Sprintf("S3InstanceRef: %s not found in cache", s3InstanceRef)} + logger.Error(err, fmt.Sprintf("S3InstanceRef: %s not found in cache", s3InstanceRef)) + return nil, err + } + logger.Info(fmt.Sprintf("Check if resource %s can use S3Instance %s", ressourceName, s3InstanceRef)) + if utils.IsAllowedNamespaces(ressourceNamespace, s3Client.GetConfig().AllowedNamespaces) { + return s3Client, nil + } else { + err := &S3ClientNotFound{Reason: fmt.Sprintf("Client %s is not allowed in this namespace", s3InstanceRef)} + return nil, err + } +} + +func GetS3InstanceRefName(ressourceS3InstanceRef string, ressourceNamespace string) string { + s3InstanceRef := ressourceS3InstanceRef + if !strings.Contains(ressourceS3InstanceRef, "/") { + s3InstanceRef = ressourceNamespace + "/" + ressourceS3InstanceRef + } + return s3InstanceRef +} + 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/const.go b/internal/utils/const.go deleted file mode 100644 index e984f72..0000000 --- a/internal/utils/const.go +++ /dev/null @@ -1,7 +0,0 @@ -package utils - -const S3OperatorBucketLabelSelectorKey = "s3operator.bucket.managed-by" -const S3OperatorPathLabelSelectorKey = "s3operator.path.managed-by" -const S3OperatorPolicyLabelSelectorKey = "s3operator.policy.managed-by" -const S3OperatorUserLabelSelectorKey = "s3operator.user.managed-by" -const S3OperatorS3InstanceLabelSelectorKey = "s3operator.s3Instance.managed-by" diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 4f69b74..26b6397 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "strings" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -27,3 +28,18 @@ func UpdateConditions(existingConditions []metav1.Condition, newCondition metav1 return append([]metav1.Condition{newCondition}, existingConditions...) } + +func IsAllowedNamespaces(namespace string, namespaces []string) bool { + for _, allowedNamespace := range namespaces { + if strings.HasPrefix(allowedNamespace, "*") && strings.HasSuffix(allowedNamespace, "*") { + return strings.Contains(namespace, strings.TrimSuffix(strings.TrimPrefix(allowedNamespace, "*"), "*")) + } else if strings.HasPrefix(allowedNamespace, "*") { + return strings.HasSuffix(namespace, strings.TrimPrefix(allowedNamespace, "*")) + } else if strings.HasSuffix(allowedNamespace, "*") { + return strings.HasPrefix(namespace, strings.TrimSuffix(allowedNamespace, "*")) + } else { + return namespace == allowedNamespace + } + } + return false +} 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)