diff --git a/Dockerfile b/Dockerfile index 31c5110..d45a843 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,8 @@ RUN go mod download COPY main.go main.go COPY api/ api/ COPY controllers/ controllers/ +COPY internal/ internal/ + # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/README.md b/README.md index b49de8e..d19c856 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This Operator SDK based tool aims at managing S3 related resources (buckets, pol ## At a glance -- Current S3 providers : [Minio](https://github.com/InseeFrLab/s3-operator/blob/main/controllers/s3/factory/minioS3Client.go) +- Current S3 providers : [Minio](https://github.com/InseeFrLab/s3-operator/blob/main/internal/s3/factory/minioS3Client.go) - Currently managed S3 resources : [buckets](https://github.com/InseeFrLab/s3-operator/blob/main/api/v1alpha1/bucket_types.go), [policies](https://github.com/InseeFrLab/s3-operator/blob/main/api/v1alpha1/policy_types.go) ## Compatibility @@ -21,7 +21,8 @@ At its heart, the operator revolves around CRDs that match S3 resources : - `buckets.s3.onyxia.sh` - `policies.s3.onyxia.sh` - `paths.s3.onyxia.sh` -- `users.s3.onyxia.sh` +- `s3Users.s3.onyxia.sh` +- `s3Instances.s3.onyxia.sh` The custom resources based on these CRDs are a somewhat simplified projection of the real S3 resources. From the operator's point of view : @@ -29,6 +30,7 @@ The custom resources based on these CRDs are a somewhat simplified projection of - A `Policy` CR matches a "canned" policy (not a bucket policy, but a global one, that can be attached to a user), and has a name, and its actual content (IAM JSON) - A `Path` CR matches a set of paths inside of a policy. This is akin to the `paths` property of the `Bucket` CRD, except `Path` is not responsible for Bucket creation. - A `S3User` CR matches a user in the s3 server, and has a name, a set of policy and a set of group. +- A `S3Instance` CR matches a s3Instance. Each custom resource based on these CRDs on Kubernetes is to be matched with a resource on the S3 instance. If the CR and the corresponding S3 resource diverge, the operator will create or update the S3 resource to bring it back to. @@ -72,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 | - +| 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: @@ -147,6 +136,34 @@ The Operator need at least this rights: - The same will happen if you modify a CR - the operator will adjust the S3 bucket or policy accordingly - with the notable exception that it will not delete paths for buckets. - Upon deleting a CR, the corresponding bucket or policy will be left as is, as mentioned in the [*Description* section above](#description) +An instance of S3Operator can manage multiple S3. On each resource created you can set where to create it. To add multiple instance of S3 see S3Instance example. On each object deployed you can attach it to an existing s3Instance. If no instance is set on the resource, S3Operator will failback to default instance configured by env var. + +### S3Instance example + +```yaml +apiVersion: s3.onyxia.sh/v1alpha1 +kind: S3Instance +metadata: + labels: + app.kubernetes.io/name: bucket + app.kubernetes.io/instance: bucket-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 of the S3Instance +spec: + s3Provider: minio # Type of the Provider. Can be "mockedS3Provider" or "minio" + 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 + 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 ```yaml @@ -182,6 +199,10 @@ spec: quota: default: 10000000 # override: 20000000 + + # Optionnal, let empty if you have configured the default s3 else use an existing s3Instance + s3InstanceRef: "s3-default-instance" + ``` @@ -202,6 +223,9 @@ spec: # Policy name (on S3 server, as opposed to the name of the CR) name: dummy-policy + # Optionnal, let empty if you have configured the default s3 else use an existing s3Instance + s3InstanceRef: "s3-default-instance" + # Content of the policy, as a multiline string # This should be IAM compliant JSON - follow the guidelines of the actual # S3 provider you're using, as sometimes only a subset is available. @@ -245,6 +269,8 @@ spec: - /home/alice - /home/bob + # Optionnal, let empty if you have configured the default s3 else use an existing s3Instance + s3InstanceRef: "s3-default-instance" ``` @@ -266,11 +292,20 @@ spec: policies: - policy-example1 - policy-example2 + # Optionnal, let empty if you have configured the default s3 else use an existing s3Instance + s3InstanceRef: "s3-default-instance" ``` 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 092aac6..71bddee 100644 --- a/api/v1alpha1/bucket_types.go +++ b/api/v1alpha1/bucket_types.go @@ -36,6 +36,14 @@ type BucketSpec struct { // +kubebuilder:validation:Optional Paths []string `json:"paths,omitempty"` + // s3InstanceRef where create the bucket + // +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 Quota Quota `json:"quota"` @@ -43,7 +51,7 @@ type BucketSpec struct { // BucketStatus defines the observed state of Bucket type BucketStatus struct { - // Status management using Conditions. + // Status management using Conditions. // See also : https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } diff --git a/api/v1alpha1/path_types.go b/api/v1alpha1/path_types.go index 58f5aad..f920a6e 100644 --- a/api/v1alpha1/path_types.go +++ b/api/v1alpha1/path_types.go @@ -35,6 +35,14 @@ type PathSpec struct { // Paths (folders) to create inside the bucket // +kubebuilder:validation:Optional Paths []string `json:"paths,omitempty"` + + // s3InstanceRef where create the Paths + // +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"` } // PathStatus defines the observed state of Path diff --git a/api/v1alpha1/policy_types.go b/api/v1alpha1/policy_types.go index 6862f5e..f594751 100644 --- a/api/v1alpha1/policy_types.go +++ b/api/v1alpha1/policy_types.go @@ -35,11 +35,19 @@ type PolicySpec struct { // +kubebuilder:validation:Required // Content of the policy (IAM JSON format) PolicyContent string `json:"policyContent"` + + // s3InstanceRef where create the Policy + // +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"` } // PolicyStatus defines the observed state of Policy type PolicyStatus struct { - // Status management using Conditions. + // Status management using Conditions. // See also : https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } diff --git a/api/v1alpha1/s3instance_types.go b/api/v1alpha1/s3instance_types.go new file mode 100644 index 0000000..db52d2c --- /dev/null +++ b/api/v1alpha1/s3instance_types.go @@ -0,0 +1,103 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// S3InstanceSpec defines the desired state of S3Instance +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,omitempty"` + + // url of the S3Instance + // +kubebuilder:validation:Required + Url string `json:"url"` + + // Ref to Secret associated to the S3Instance containing accessKey and secretKey + // +kubebuilder:validation:Required + SecretRef string `json:"secretRef"` + + // region associated to the S3Instance + // +kubebuilder:validation:Optional + Region string `json:"region,omitempty"` + + // Secret containing key ca.crt with the certificate associated to the S3InstanceUrl + // +kubebuilder:validation:Optional + CaCertSecretRef string `json:"caCertSecretRef,omitempty"` + + // AllowedNamespaces to use this S3InstanceUrl if empty only the namespace of this instance url is allowed to use it + // +kubebuilder:validation:Optional + AllowedNamespaces []string `json:"allowedNamespaces,omitempty"` + + // BucketDeletionEnabled Trigger bucket deletion on the S3 backend upon CR deletion. Will fail if bucket is not empty. + // +kubebuilder:default=false + BucketDeletionEnabled bool `json:"bucketDeletionEnabled,omitempty"` + + // PolicyDeletionEnabled Trigger policy deletion on the S3 backend upon CR deletion. + // +kubebuilder:default=false + PolicyDeletionEnabled bool `json:"policyDeletionEnabled,omitempty"` + + // PathDeletionEnabled Trigger path deletion on the S3 backend upon CR deletion. Limited to deleting the `.keep` files used by the operator. + // +kubebuilder:default=false + PathDeletionEnabled bool `json:"pathDeletionEnabled,omitempty"` + + // S3UserDeletionEnabled Trigger S3 deletion on the S3 backend upon CR deletion. + // +kubebuilder:default=false + S3UserDeletionEnabled bool `json:"s3UserDeletionEnabled,omitempty"` +} + +// S3InstanceStatus defines the observed state of S3Instance +type S3InstanceStatus struct { + // Status management using Conditions. + // See also : https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// S3Instance is the Schema for the S3Instances API +type S3Instance struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec S3InstanceSpec `json:"spec,omitempty"` + Status S3InstanceStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// S3InstanceList contains a list of S3Instance +type S3InstanceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []S3Instance `json:"items"` +} + +func init() { + SchemeBuilder.Register(&S3Instance{}, &S3InstanceList{}) +} diff --git a/api/v1alpha1/s3user_types.go b/api/v1alpha1/s3user_types.go index ac40da6..94f844a 100644 --- a/api/v1alpha1/s3user_types.go +++ b/api/v1alpha1/s3user_types.go @@ -37,6 +37,14 @@ type S3UserSpec struct { // SecretName associated to the S3User // +kubebuilder:validation:Optional SecretName string `json:"secretName"` + + // s3InstanceRef where create the user + // +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"` } // S3UserStatus defines the observed state of S3User 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 6761cdf..f9ca52d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -340,6 +340,107 @@ func (in *Quota) DeepCopy() *Quota { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3Instance) DeepCopyInto(out *S3Instance) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3Instance. +func (in *S3Instance) DeepCopy() *S3Instance { + if in == nil { + return nil + } + out := new(S3Instance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *S3Instance) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3InstanceList) DeepCopyInto(out *S3InstanceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]S3Instance, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3InstanceList. +func (in *S3InstanceList) DeepCopy() *S3InstanceList { + if in == nil { + return nil + } + out := new(S3InstanceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *S3InstanceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// 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.AllowedNamespaces != nil { + in, out := &in.AllowedNamespaces, &out.AllowedNamespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3InstanceSpec. +func (in *S3InstanceSpec) DeepCopy() *S3InstanceSpec { + if in == nil { + return nil + } + out := new(S3InstanceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *S3InstanceStatus) DeepCopyInto(out *S3InstanceStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3InstanceStatus. +func (in *S3InstanceStatus) DeepCopy() *S3InstanceStatus { + if in == nil { + return nil + } + out := new(S3InstanceStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *S3User) DeepCopyInto(out *S3User) { *out = *in @@ -439,4 +540,4 @@ func (in *S3UserStatus) DeepCopy() *S3UserStatus { out := new(S3UserStatus) in.DeepCopyInto(out) return out -} \ No newline at end of file +} diff --git a/config/crd/bases/s3.onyxia.sh_buckets.yaml b/config/crd/bases/s3.onyxia.sh_buckets.yaml index 6b2cbcd..1d2daf6 100644 --- a/config/crd/bases/s3.onyxia.sh_buckets.yaml +++ b/config/crd/bases/s3.onyxia.sh_buckets.yaml @@ -57,9 +57,20 @@ spec: required: - 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 bc55aa3..b825e91 100644 --- a/config/crd/bases/s3.onyxia.sh_paths.yaml +++ b/config/crd/bases/s3.onyxia.sh_paths.yaml @@ -43,6 +43,16 @@ spec: items: 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 aa78618..9ee65c5 100644 --- a/config/crd/bases/s3.onyxia.sh_policies.yaml +++ b/config/crd/bases/s3.onyxia.sh_policies.yaml @@ -41,6 +41,16 @@ spec: policyContent: 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 new file mode 100644 index 0000000..4627360 --- /dev/null +++ b/config/crd/bases/s3.onyxia.sh_s3instances.yaml @@ -0,0 +1,175 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: s3instances.s3.onyxia.sh +spec: + group: s3.onyxia.sh + names: + kind: S3Instance + listKind: S3InstanceList + plural: s3instances + singular: s3instance + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: S3Instance is the Schema for the S3Instances API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: S3InstanceSpec defines the desired state of S3Instance + properties: + 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 + 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 + url: + description: url of the S3Instance + type: string + required: + - bucketDeletionEnabled + - pathDeletionEnabled + - policyDeletionEnabled + - s3Provider + - s3UserDeletionEnabled + - secretRef + - url + type: object + status: + description: S3InstanceStatus defines the observed state of S3Instance + properties: + conditions: + description: 'Status management using Conditions. See also : https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties' + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/s3.onyxia.sh_s3users.yaml b/config/crd/bases/s3.onyxia.sh_s3users.yaml index 2c46a98..5ed8813 100644 --- a/config/crd/bases/s3.onyxia.sh_s3users.yaml +++ b/config/crd/bases/s3.onyxia.sh_s3users.yaml @@ -43,6 +43,16 @@ spec: items: 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 5190141..fc25de9 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -6,27 +6,26 @@ metadata: name: manager-role rules: - apiGroups: - - s3.onyxia.sh + - "" resources: - - S3User + - secrets verbs: - create - delete - get - list - - patch - update - watch - apiGroups: - - s3.onyxia.sh + - "" resources: - - S3User/finalizers + - secrets/finalizers verbs: - update - apiGroups: - - s3.onyxia.sh + - "" resources: - - S3User/status + - secrets/status verbs: - get - patch @@ -109,3 +108,55 @@ rules: - get - patch - update +- apiGroups: + - s3.onyxia.sh + resources: + - s3instances + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - s3.onyxia.sh + resources: + - s3instances/finalizers + verbs: + - update +- apiGroups: + - s3.onyxia.sh + resources: + - s3instances/status + verbs: + - get + - patch + - update +- apiGroups: + - s3.onyxia.sh + resources: + - s3users + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - s3.onyxia.sh + resources: + - s3users/finalizers + verbs: + - update +- apiGroups: + - s3.onyxia.sh + resources: + - s3users/status + verbs: + - get + - patch + - update diff --git a/config/samples/s3.onyxia.sh_v1alpha1_bucket.yaml b/config/samples/s3.onyxia.sh_v1alpha1_bucket.yaml index 4d55426..1f201b8 100644 --- a/config/samples/s3.onyxia.sh_v1alpha1_bucket.yaml +++ b/config/samples/s3.onyxia.sh_v1alpha1_bucket.yaml @@ -1,12 +1,6 @@ apiVersion: s3.onyxia.sh/v1alpha1 kind: Bucket metadata: - labels: - app.kubernetes.io/name: bucket - app.kubernetes.io/instance: bucket-sample - app.kubernetes.io/part-of: s3-operator - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: s3-operator name: bucket-sample spec: # Bucket name (on S3 server, as opposed to the name of the CR) diff --git a/config/samples/s3.onyxia.sh_v1alpha1_path.yaml b/config/samples/s3.onyxia.sh_v1alpha1_path.yaml index 86cf9af..fa4a258 100644 --- a/config/samples/s3.onyxia.sh_v1alpha1_path.yaml +++ b/config/samples/s3.onyxia.sh_v1alpha1_path.yaml @@ -1,12 +1,6 @@ apiVersion: s3.onyxia.sh/v1alpha1 kind: Path metadata: - labels: - app.kubernetes.io/name: path - app.kubernetes.io/instance: path-sample - app.kubernetes.io/part-of: s3-operator - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: s3-operator name: path-sample spec: # Bucket name (on S3 server, as opposed to the name of the CR) diff --git a/config/samples/s3.onyxia.sh_v1alpha1_policy.yaml b/config/samples/s3.onyxia.sh_v1alpha1_policy.yaml index bff4f74..7e1ca77 100644 --- a/config/samples/s3.onyxia.sh_v1alpha1_policy.yaml +++ b/config/samples/s3.onyxia.sh_v1alpha1_policy.yaml @@ -1,12 +1,6 @@ apiVersion: s3.onyxia.sh/v1alpha1 kind: Policy metadata: - labels: - app.kubernetes.io/name: policy - app.kubernetes.io/instance: policy-sample - app.kubernetes.io/part-of: s3-operator - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: s3-operator name: policy-sample spec: # Policy name (on S3 server, as opposed to the name of the CR) diff --git a/config/samples/s3.onyxia.sh_v1alpha1_s3instance.yaml b/config/samples/s3.onyxia.sh_v1alpha1_s3instance.yaml new file mode 100644 index 0000000..131a555 --- /dev/null +++ b/config/samples/s3.onyxia.sh_v1alpha1_s3instance.yaml @@ -0,0 +1,31 @@ +apiVersion: s3.onyxia.sh/v1alpha1 +kind: S3Instance +metadata: + name: s3instance-sample +spec: + s3Provider: minio + 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 +stringData: + ca.crt: | + -----BEGIN CERTIFICATE----- + ... + -----END CERTIFICATE----- diff --git a/config/samples/s3.onyxia.sh_v1alpha1_s3user.yaml b/config/samples/s3.onyxia.sh_v1alpha1_s3user.yaml index 24a6458..fc11c14 100644 --- a/config/samples/s3.onyxia.sh_v1alpha1_s3user.yaml +++ b/config/samples/s3.onyxia.sh_v1alpha1_s3user.yaml @@ -1,12 +1,6 @@ apiVersion: s3.onyxia.sh/v1alpha1 kind: S3User metadata: - labels: - app.kubernetes.io/name: user - app.kubernetes.io/instance: user-sample - app.kubernetes.io/part-of: s3-operator - app.kubernetes.io/managed-by: kustomize - app.kubernetes.io/created-by: s3-operator name: user-sample spec: accessKey: user-sample diff --git a/controllers/bucket_controller.go b/controllers/bucket_controller.go index f6a88bd..799471a 100644 --- a/controllers/bucket_controller.go +++ b/controllers/bucket_controller.go @@ -21,7 +21,11 @@ import ( "fmt" "time" - "k8s.io/apimachinery/pkg/api/errors" + s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" + controllerhelpers "github.com/InseeFrLab/s3-operator/internal/controllerhelper" + + utils "github.com/InseeFrLab/s3-operator/internal/utils" + 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" @@ -32,26 +36,22 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" - - s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" - "github.com/InseeFrLab/s3-operator/controllers/s3/factory" - "github.com/InseeFrLab/s3-operator/controllers/utils" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // BucketReconciler reconciles a Bucket object type BucketReconciler struct { client.Client - Scheme *runtime.Scheme - S3Client factory.S3Client - BucketDeletion bool + Scheme *runtime.Scheme + ReconcilePeriod time.Duration } +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. // @@ -64,7 +64,7 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr 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 } @@ -72,38 +72,9 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } - // 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(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 { @@ -111,12 +82,42 @@ 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 + } } - // Bucket lifecycle management (other than deletion) starts here + // // 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) + } + return r.handleReconciliation(ctx, bucketResource) + +} + +func (r *BucketReconciler) handleReconciliation(ctx context.Context, bucketResource *s3v1alpha1.Bucket) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + s3Client, err := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, bucketResource.Name, bucketResource.Namespace, bucketResource.Spec.S3InstanceRef) + if err != nil { + 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 // Check bucket existence on the S3 server - found, err := r.S3Client.BucketExists(bucketResource.Spec.Name) + found, err := s3Client.BucketExists(bucketResource.Spec.Name) if err != nil { logger.Error(err, "an error occurred while checking the existence of a bucket", "bucket", bucketResource.Spec.Name) return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "BucketExistenceCheckFailed", @@ -125,43 +126,29 @@ 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 = r.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 = r.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 = r.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 := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, bucketResource.Name, bucketResource.Namespace, bucketResource.Spec.S3InstanceRef) + if err != nil { + 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 // its corresponding custom resource, and update it in case the CR has changed. // Checking effectiveQuota existence on the bucket - effectiveQuota, err := r.S3Client.GetQuota(bucketResource.Spec.Name) + effectiveQuota, err := s3Client.GetQuota(bucketResource.Spec.Name) if err != nil { logger.Error(err, "an error occurred while getting the quota for a bucket", "bucket", bucketResource.Spec.Name) return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "BucketQuotaCheckFailed", @@ -178,7 +165,7 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } if effectiveQuota != quotaToResetTo { - err = r.S3Client.SetQuota(bucketResource.Spec.Name, quotaToResetTo) + err = s3Client.SetQuota(bucketResource.Spec.Name, quotaToResetTo) if err != nil { logger.Error(err, "an error occurred while resetting the quota for a bucket", "bucket", bucketResource.Spec.Name, "quotaToResetTo", quotaToResetTo) return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "BucketQuotaUpdateFailed", @@ -194,7 +181,7 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr // But then again, some buckets will likely be filled with many objects outside the // scope of the CR, so getting all of them might be even more costly. for _, pathInCr := range bucketResource.Spec.Paths { - pathExists, err := r.S3Client.PathExists(bucketResource.Spec.Name, pathInCr) + pathExists, err := s3Client.PathExists(bucketResource.Spec.Name, pathInCr) if err != nil { logger.Error(err, "an error occurred while checking a path's existence on a bucket", "bucket", bucketResource.Spec.Name, "path", pathInCr) return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "BucketPathCheckFailed", @@ -202,7 +189,7 @@ func (r *BucketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } if !pathExists { - err = r.S3Client.CreatePath(bucketResource.Spec.Name, pathInCr) + err = s3Client.CreatePath(bucketResource.Spec.Name, pathInCr) if err != nil { logger.Error(err, "an error occurred while creating a path on a bucket", "bucket", bucketResource.Spec.Name, "path", pathInCr) return r.SetBucketStatusConditionAndUpdate(ctx, bucketResource, "OperatorFailed", metav1.ConditionFalse, "BucketPathCreationFailed", @@ -214,7 +201,79 @@ 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 := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, bucketResource.Name, bucketResource.Namespace, bucketResource.Spec.S3InstanceRef) + if err != nil { + 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) { + + 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.* @@ -236,9 +295,16 @@ func (r *BucketReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *BucketReconciler) finalizeBucket(bucketResource *s3v1alpha1.Bucket) error { - if r.BucketDeletion { - return r.S3Client.DeleteBucket(bucketResource.Spec.Name) +func (r *BucketReconciler) finalizeBucket(ctx context.Context, bucketResource *s3v1alpha1.Bucket) error { + logger := log.FromContext(ctx) + + s3Client, err := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, bucketResource.Name, bucketResource.Namespace, bucketResource.Spec.S3InstanceRef) + if err != nil { + logger.Error(err, "an error occurred while getting s3Client") + return err + } + if s3Client.GetConfig().BucketDeletionEnabled { + return s3Client.DeleteBucket(bucketResource.Spec.Name) } return nil } @@ -263,5 +329,5 @@ func (r *BucketReconciler) SetBucketStatusConditionAndUpdate(ctx context.Context logger.Error(err, "an error occurred while updating the status of the bucket resource") return ctrl.Result{}, utilerrors.NewAggregate([]error{err, srcError}) } - return ctrl.Result{}, srcError + return ctrl.Result{RequeueAfter: r.ReconcilePeriod}, srcError } diff --git a/controllers/path_controller.go b/controllers/path_controller.go index d789d5b..5db20d5 100644 --- a/controllers/path_controller.go +++ b/controllers/path_controller.go @@ -21,7 +21,7 @@ import ( "fmt" "time" - "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" @@ -32,18 +32,18 @@ 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/controllers/s3/factory" - "github.com/InseeFrLab/s3-operator/controllers/utils" + controllerhelpers "github.com/InseeFrLab/s3-operator/internal/controllerhelper" + "github.com/InseeFrLab/s3-operator/internal/utils" ) // PathReconciler reconciles a Path object type PathReconciler struct { client.Client - Scheme *runtime.Scheme - S3Client factory.S3Client - PathDeletion bool + Scheme *runtime.Scheme + ReconcilePeriod time.Duration } //+kubebuilder:rbac:groups=s3.onyxia.sh,resources=paths,verbs=get;list;watch;create;update;patch;delete @@ -64,7 +64,7 @@ func (r *PathReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. 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 } @@ -72,36 +72,6 @@ func (r *PathReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. return ctrl.Result{}, err } - // 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(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) @@ -112,12 +82,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 := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, pathResource.Name, pathResource.Namespace, pathResource.Spec.S3InstanceRef) + if err != nil { + 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 // Check bucket existence on the S3 server - bucketFound, err := r.S3Client.BucketExists(pathResource.Spec.BucketName) + bucketFound, err := s3Client.BucketExists(pathResource.Spec.BucketName) if err != nil { logger.Error(err, "an error occurred while checking the existence of a bucket", "bucket", pathResource.Spec.BucketName) return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorFailed", metav1.ConditionFalse, "BucketExistenceCheckFailed", @@ -141,7 +142,7 @@ func (r *PathReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. // But then again, some buckets will likely be filled with many objects outside the // scope of the CR, so getting all of them might be even more costly. for _, pathInCr := range pathResource.Spec.Paths { - pathExists, err := r.S3Client.PathExists(pathResource.Spec.BucketName, pathInCr) + pathExists, err := s3Client.PathExists(pathResource.Spec.BucketName, pathInCr) if err != nil { logger.Error(err, "an error occurred while checking a path's existence on a bucket", "bucket", pathResource.Spec.BucketName, "path", pathInCr) return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorFailed", metav1.ConditionFalse, "PathCheckFailed", @@ -149,7 +150,7 @@ func (r *PathReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. } if !pathExists { - err = r.S3Client.CreatePath(pathResource.Spec.BucketName, pathInCr) + err = s3Client.CreatePath(pathResource.Spec.BucketName, pathInCr) if err != nil { logger.Error(err, "an error occurred while creating a path on a bucket", "bucket", pathResource.Spec.BucketName, "path", pathInCr) return r.SetPathStatusConditionAndUpdate(ctx, pathResource, "OperatorFailed", metav1.ConditionFalse, "PathCreationFailed", @@ -161,7 +162,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. @@ -183,19 +222,25 @@ func (r *PathReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *PathReconciler) finalizePath(pathResource *s3v1alpha1.Path) error { - logger := log.Log.WithValues("controller", "path") - if r.PathDeletion { +func (r *PathReconciler) finalizePath(ctx context.Context, pathResource *s3v1alpha1.Path) error { + logger := log.FromContext(ctx) + s3Client, err := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, pathResource.Name, pathResource.Namespace, pathResource.Spec.S3InstanceRef) + if err != nil { + logger.Error(err, "an error occurred while getting s3Client") + return err + } + + if s3Client.GetConfig().PathDeletionEnabled { var failedPaths []string = make([]string, 0) for _, path := range pathResource.Spec.Paths { - pathExists, err := r.S3Client.PathExists(pathResource.Spec.BucketName, path) + pathExists, err := s3Client.PathExists(pathResource.Spec.BucketName, path) if err != nil { logger.Error(err, "finalize : an error occurred while checking a path's existence on a bucket", "bucket", pathResource.Spec.BucketName, "path", path) } if pathExists { - err = r.S3Client.DeletePath(pathResource.Spec.BucketName, path) + err = s3Client.DeletePath(pathResource.Spec.BucketName, path) if err != nil { failedPaths = append(failedPaths, path) } @@ -229,5 +274,5 @@ func (r *PathReconciler) SetPathStatusConditionAndUpdate(ctx context.Context, pa logger.Error(err, "an error occurred while updating the status of the path resource") return ctrl.Result{}, utilerrors.NewAggregate([]error{err, srcError}) } - return ctrl.Result{}, srcError + return ctrl.Result{RequeueAfter: r.ReconcilePeriod}, srcError } diff --git a/controllers/policy_controller.go b/controllers/policy_controller.go index 1dd597a..cf5141e 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,18 +35,18 @@ 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/controllers/s3/factory" - "github.com/InseeFrLab/s3-operator/controllers/utils" + controllerhelpers "github.com/InseeFrLab/s3-operator/internal/controllerhelper" + "github.com/InseeFrLab/s3-operator/internal/utils" ) // PolicyReconciler reconciles a Policy object type PolicyReconciler struct { client.Client - Scheme *runtime.Scheme - S3Client factory.S3Client - PolicyDeletion bool + Scheme *runtime.Scheme + ReconcilePeriod time.Duration } //+kubebuilder:rbac:groups=s3.onyxia.sh,resources=policies,verbs=get;list;watch;create;update;patch;delete @@ -67,7 +67,7 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr 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 } @@ -75,36 +75,6 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } - // 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(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) @@ -115,12 +85,43 @@ 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 := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, policyResource.Name, policyResource.Namespace, policyResource.Spec.S3InstanceRef) + if err != nil { + 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 - effectivePolicy, err := r.S3Client.GetPolicyInfo(policyResource.Spec.Name) + effectivePolicy, err := s3Client.GetPolicyInfo(policyResource.Spec.Name) // If the policy does not exist on S3... if err != nil { @@ -132,7 +133,7 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr if effectivePolicy == nil { // Policy creation using info from the CR - err = r.S3Client.CreateOrUpdatePolicy(policyResource.Spec.Name, policyResource.Spec.PolicyContent) + err = s3Client.CreateOrUpdatePolicy(policyResource.Spec.Name, policyResource.Spec.PolicyContent) if err != nil { logger.Error(err, "an error occurred while creating the policy", "policy", policyResource.Spec.Name) return r.SetPolicyStatusConditionAndUpdate(ctx, policyResource, "OperatorFailed", metav1.ConditionFalse, "PolicyCreationFailed", @@ -160,7 +161,7 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr } // If not we update the policy to match the CR - err = r.S3Client.CreateOrUpdatePolicy(policyResource.Spec.Name, policyResource.Spec.PolicyContent) + err = s3Client.CreateOrUpdatePolicy(policyResource.Spec.Name, policyResource.Spec.PolicyContent) if err != nil { logger.Error(err, "an error occurred while updating the policy", "policy", policyResource.Spec.Name) return r.SetPolicyStatusConditionAndUpdate(ctx, policyResource, "OperatorFailed", metav1.ConditionFalse, "PolicyUpdateFailed", @@ -172,6 +173,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). @@ -210,9 +245,15 @@ func IsPolicyMatchingWithCustomResource(policyResource *s3v1alpha1.Policy, effec return bytes.Equal(buffer.Bytes(), marshalled), nil } -func (r *PolicyReconciler) finalizePolicy(policyResource *s3v1alpha1.Policy) error { - if r.PolicyDeletion { - return r.S3Client.DeletePolicy(policyResource.Spec.Name) +func (r *PolicyReconciler) finalizePolicy(ctx context.Context, policyResource *s3v1alpha1.Policy) error { + logger := log.FromContext(ctx) + s3Client, err := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, policyResource.Name, policyResource.Namespace, policyResource.Spec.S3InstanceRef) + if err != nil { + logger.Error(err, "an error occurred while getting s3Client") + return err + } + if s3Client.GetConfig().PolicyDeletionEnabled { + return s3Client.DeletePolicy(policyResource.Spec.Name) } return nil } @@ -237,5 +278,5 @@ func (r *PolicyReconciler) SetPolicyStatusConditionAndUpdate(ctx context.Context logger.Error(err, "an error occurred while updating the status of the policy resource") return ctrl.Result{}, utilerrors.NewAggregate([]error{err, srcError}) } - return ctrl.Result{}, srcError + return ctrl.Result{RequeueAfter: r.ReconcilePeriod}, srcError } diff --git a/controllers/s3instance_controller.go b/controllers/s3instance_controller.go new file mode 100644 index 0000000..8b9c059 --- /dev/null +++ b/controllers/s3instance_controller.go @@ -0,0 +1,335 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "time" + + 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" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "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" + controllerhelpers "github.com/InseeFrLab/s3-operator/internal/controllerhelper" +) + +// S3InstanceReconciler reconciles a S3Instance object +type S3InstanceReconciler struct { + client.Client + Scheme *runtime.Scheme + ReconcilePeriod time.Duration +} + +const ( + s3InstanceFinalizer = "s3.onyxia.sh/finalizer" +) + +//+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. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile +func (r *S3InstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + // Checking for s3InstanceResource existence + s3InstanceResource := &s3v1alpha1.S3Instance{} + err := r.Get(ctx, req.NamespacedName, s3InstanceResource) + if err != nil { + 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, "Failed to get S3InstanceResource", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, err + } + + // 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 + } + + // 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, req, s3InstanceResource) + } + + // Reconciliation starts here + return r.handleReconciliation(ctx, req, s3InstanceResource) + +} + +func (r *S3InstanceReconciler) handleReconciliation(ctx context.Context, req reconcile.Request, s3InstanceResource *s3v1alpha1.S3Instance) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + s3Client, err := controllerhelpers.GetS3ClientFromS3Instance(ctx, r.Client, s3InstanceResource) + + if err != nil { + 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) + } + + _, err = s3Client.ListBuckets() + if err != nil { + logger.Error(err, "Could not generate s3Instance", "s3InstanceName", s3InstanceResource.Name, "NamespacedName", req.NamespacedName.String()) + return r.SetReconciledCondition(ctx, req, s3InstanceResource, s3v1alpha1.CreationFailure, + "Failed to generate S3Instance ", err) + } + + return r.SetReconciledCondition(ctx, req, s3InstanceResource, s3v1alpha1.Reconciled, "S3Instance instance reconciled", nil) + +} + +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) { + 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 + } + + //Remove s3InstanceFinalizer. Once all finalizers have been removed, the object will be deleted. + 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 +} + +// SetupWithManager sets up the controller with the Manager.* +func (r *S3InstanceReconciler) SetupWithManager(mgr ctrl.Manager) error { + // filterLogger := ctrl.Log.WithName("filterEvt") + return ctrl.NewControllerManagedBy(mgr). + For(&s3v1alpha1.S3Instance{}). + // 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 { + return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() + }, + DeleteFunc: func(e event.DeleteEvent) bool { + // Evaluates to false if the object has been confirmed deleted. + return !e.DeleteStateUnknown + }, + }). + WithOptions(controller.Options{MaxConcurrentReconciles: 10}). + Complete(r) +} + +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) + + var changed bool + + if err != nil { + 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 s3Instance status", "NamespacedName", req.NamespacedName.String()) + return ctrl.Result{}, errStatusUpdate + } + } + + return ctrl.Result{RequeueAfter: r.ReconcilePeriod}, err +} + +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) + } + + for _, bucket := range bucketLists.Items { + if controllerhelpers.GetS3InstanceRefInfo(bucket.Spec.S3InstanceRef, bucket.Namespace).String() == s3InstanceResource.Namespace+"/"+s3InstanceResource.Name { + found++ + } + } + + 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 controllerhelpers.GetS3InstanceRefInfo(policy.Spec.S3InstanceRef, policy.Namespace).String() == 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 controllerhelpers.GetS3InstanceRefInfo(s3User.Spec.S3InstanceRef, s3User.Namespace).String() == 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 controllerhelpers.GetS3InstanceRefInfo(path.Spec.S3InstanceRef, path.Namespace).String() == 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 204b6a2..0d82e82 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" @@ -39,27 +39,29 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" - "github.com/InseeFrLab/s3-operator/controllers/s3/factory" - utils "github.com/InseeFrLab/s3-operator/controllers/utils" - password "github.com/InseeFrLab/s3-operator/controllers/utils/password" + controllerhelpers "github.com/InseeFrLab/s3-operator/internal/controllerhelper" + utils "github.com/InseeFrLab/s3-operator/internal/utils" + password "github.com/InseeFrLab/s3-operator/internal/utils/password" ) // S3UserReconciler reconciles a S3User object type S3UserReconciler struct { client.Client Scheme *runtime.Scheme - S3Client factory.S3Client - S3UserDeletion bool OverrideExistingSecret bool + ReconcilePeriod time.Duration } const ( userFinalizer = "s3.onyxia.sh/userFinalizer" ) -//+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. @@ -73,7 +75,7 @@ func (r *S3UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr 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 } @@ -81,13 +83,6 @@ func (r *S3UserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return ctrl.Result{}, err } - // 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") @@ -99,30 +94,63 @@ 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 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 + // 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 := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, userResource.Name, userResource.Namespace, userResource.Spec.S3InstanceRef) + if err != nil { + 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) } - // Check user existence on the S3 server - found, err := r.S3Client.UserExist(userResource.Spec.AccessKey) + found, err := s3Client.UserExist(userResource.Spec.AccessKey) if err != nil { logger.Error(err, "an error occurred while checking the existence of a user", "user", userResource.Name) return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "S3UserExistenceCheckFailed", 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) - // --- Begin Secret management section + // Create S3Client + s3Client, err := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, userResource.Name, userResource.Namespace, userResource.Spec.S3InstanceRef) + if err != nil { + 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) + } userOwnedSecret, err := r.getUserSecret(ctx, userResource) if err != nil { @@ -130,13 +158,13 @@ func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResourc logger.Error(err, "An error occurred when trying to obtain the user's secret. The user will be deleted from S3 backend and recreated with a secret.") r.deleteSecret(ctx, &userOwnedSecret) - err = r.S3Client.DeleteUser(userResource.Spec.AccessKey) + err = s3Client.DeleteUser(userResource.Spec.AccessKey) if err != nil { logger.Error(err, "Could not delete user on S3 server", "user", userResource.Name) 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) @@ -145,19 +173,19 @@ func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResourc if userOwnedSecret.Name == "" { logger.Info("Secret associated to user not found, user will be deleted from the S3 backend, then recreated with a secret") - err = r.S3Client.DeleteUser(userResource.Spec.AccessKey) + err = s3Client.DeleteUser(userResource.Spec.AccessKey) if err != nil { logger.Error(err, "Could not delete user on S3 server", "user", userResource.Name) 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 // contains still allow authenticating the S3User on the backend ? If not, the user is deleted and recreated. // credentialsValid, err := r.S3Client.CheckUserCredentialsValid(userResource.Name, userResource.Spec.AccessKey, string(userOwnedSecret.Data["secretKey"])) - credentialsValid, err := r.S3Client.CheckUserCredentialsValid(userResource.Name, string(userOwnedSecret.Data["accessKey"]), string(userOwnedSecret.Data["secretKey"])) + credentialsValid, err := s3Client.CheckUserCredentialsValid(userResource.Name, string(userOwnedSecret.Data["accessKey"]), string(userOwnedSecret.Data["secretKey"])) if err != nil { logger.Error(err, "An error occurred when checking if user credentials were valid", "user", userResource.Name) return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "S3UserCredentialsCheckFailed", @@ -167,21 +195,21 @@ func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResourc if !credentialsValid { logger.Info("The secret containing the credentials will be deleted, and the user will be deleted from the S3 backend, then recreated (through another reconcile)") r.deleteSecret(ctx, &userOwnedSecret) - err = r.S3Client.DeleteUser(userResource.Spec.AccessKey) + err = s3Client.DeleteUser(userResource.Spec.AccessKey) if err != nil { logger.Error(err, "Could not delete user on S3 server", "user", userResource.Name) 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) } // --- End Secret management section logger.Info("Checking user policies") - userPolicies, err := r.S3Client.GetUserPolicies(userResource.Spec.AccessKey) + userPolicies, err := s3Client.GetUserPolicies(userResource.Spec.AccessKey) if err != nil { logger.Error(err, "Could not check the user's policies") return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "S3UserPolicyCheckFailed", @@ -207,7 +235,7 @@ func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResourc } if len(policyToDelete) > 0 { - err = r.S3Client.RemovePoliciesFromUser(userResource.Spec.AccessKey, policyToDelete) + err = s3Client.RemovePoliciesFromUser(userResource.Spec.AccessKey, policyToDelete) if err != nil { logger.Error(err, "an error occurred while removing policy to user", "user", userResource.Name) return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "S3UserPolicyAppendFailed", @@ -216,7 +244,7 @@ func (r *S3UserReconciler) handleS3ExistingUser(ctx context.Context, userResourc } if len(policyToAdd) > 0 { - err := r.S3Client.AddPoliciesToUser(userResource.Spec.AccessKey, policyToAdd) + err := s3Client.AddPoliciesToUser(userResource.Spec.AccessKey, policyToAdd) if err != nil { logger.Error(err, "an error occurred while adding policy to user", "user", userResource.Name) return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "S3UserPolicyAppendFailed", @@ -238,9 +266,17 @@ 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 := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, userResource.Name, userResource.Namespace, userResource.Spec.S3InstanceRef) + if err != nil { + 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 { @@ -264,11 +300,11 @@ 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 - err = r.S3Client.CreateUser(userResource.Spec.AccessKey, secretKey) + err = s3Client.CreateUser(userResource.Spec.AccessKey, secretKey) if err != nil { logger.Error(err, "an error occurred while creating user on S3 server", "user", userResource.Name) @@ -318,7 +354,7 @@ func (r *S3UserReconciler) handleS3NewUser(ctx context.Context, userResource *s3 logger.Info(fmt.Sprintf("A secret with the name %s already exists ; it will be overwritten as per operator configuration", secret.Name)) // Creating the user - err = r.S3Client.CreateUser(userResource.Spec.AccessKey, secretKey) + err = s3Client.CreateUser(userResource.Spec.AccessKey, secretKey) if err != nil { logger.Error(err, "an error occurred while creating user on S3 server", "user", userResource.Name) @@ -350,17 +386,21 @@ func (r *S3UserReconciler) handleS3NewUser(ctx context.Context, userResource *s3 // The user will not be created, with no requeue and with two possible ways out : either toggle // OverrideExistingSecret on, or delete the S3User whose credentials are not working anyway. logger.Error(nil, fmt.Sprintf("A secret with the name %s already exists ; as the operator is configured to NOT override any pre-existing secrets, this user (%s) not be created on S3 backend until spec change (to target new secret), or until the operator configuration is changed to override existing secrets", secret.Name, userResource.Name)) - return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorSucceeded", metav1.ConditionTrue, "S3UserCreationFailedAsSecretCannotBeOverwritten", + return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "S3UserCreationFailedAsSecretCannotBeOverwritten", fmt.Sprintf("The S3User %s wasn't created, as its spec targets a secret (%s) containing invalid credentials. The user's spec should be changed to target a different secret.", userResource.Name, secret.Name), nil) - } } func (r *S3UserReconciler) addPoliciesToUser(ctx context.Context, userResource *s3v1alpha1.S3User) error { logger := log.FromContext(ctx) + // Create S3Client + s3Client, err := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, userResource.Name, userResource.Namespace, userResource.Spec.S3InstanceRef) + if err != nil { + return err + } policies := userResource.Spec.Policies if policies != nil { - err := r.S3Client.AddPoliciesToUser(userResource.Spec.AccessKey, policies) + err := s3Client.AddPoliciesToUser(userResource.Spec.AccessKey, policies) if err != nil { logger.Error(err, "an error occurred while adding policy to user", "user", userResource.Name) return err @@ -374,7 +414,7 @@ func (r *S3UserReconciler) handleS3UserDeletion(ctx context.Context, userResourc if controllerutil.ContainsFinalizer(userResource, userFinalizer) { // Run finalization logic for S3UserFinalizer. If the finalization logic fails, don't remove the finalizer so that we can retry during the next reconciliation. - if err := r.finalizeS3User(userResource); err != nil { + if err := r.finalizeS3User(ctx, userResource); err != nil { logger.Error(err, "an error occurred when attempting to finalize the user", "user", userResource.Name) return r.setS3UserStatusConditionAndUpdate(ctx, userResource, "OperatorFailed", metav1.ConditionFalse, "S3UserFinalizeFailed", fmt.Sprintf("An error occurred when attempting to delete user %s", userResource.Name), err) @@ -397,47 +437,26 @@ 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{} + secretName := userResource.Spec.SecretName + if secretName == "" { + secretName = userResource.Name + } + err := r.Get(ctx, types.NamespacedName{Namespace: userResource.Namespace, Name: 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) { @@ -509,12 +528,20 @@ func (r *S3UserReconciler) setS3UserStatusConditionAndUpdate(ctx context.Context logger.Error(err, "an error occurred while updating the status of the S3User resource") return ctrl.Result{}, utilerrors.NewAggregate([]error{err, srcError}) } - return ctrl.Result{}, srcError + + return ctrl.Result{RequeueAfter: r.ReconcilePeriod}, srcError } -func (r *S3UserReconciler) finalizeS3User(userResource *s3v1alpha1.S3User) error { - if r.S3UserDeletion { - return r.S3Client.DeleteUser(userResource.Spec.AccessKey) +func (r *S3UserReconciler) finalizeS3User(ctx context.Context, userResource *s3v1alpha1.S3User) error { + logger := log.FromContext(ctx) + // Create S3Client + s3Client, err := controllerhelpers.GetS3ClientForRessource(ctx, r.Client, userResource.Name, userResource.Namespace, userResource.Spec.S3InstanceRef) + if err != nil { + logger.Error(err, "an error occurred while getting s3Client") + return err + } + if s3Client.GetConfig().S3UserDeletionEnabled { + return s3Client.DeleteUser(userResource.Spec.AccessKey) } return nil } 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/controllerhelper/controllerhelpers.go b/internal/controllerhelper/controllerhelpers.go new file mode 100644 index 0000000..b7d8ada --- /dev/null +++ b/internal/controllerhelper/controllerhelpers.go @@ -0,0 +1,131 @@ +package controllerhelpers + +import ( + "context" + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + + s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" + s3factory "github.com/InseeFrLab/s3-operator/internal/s3/factory" + k8sapierrors "k8s.io/apimachinery/pkg/api/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/log" +) + +var ( + logger = ctrl.Log.WithValues("logger", "S3InstanceUtils") +) + +func GetS3ClientForRessource(ctx context.Context, client client.Client, ressourceName string, ressourceNamespace string, ressourceS3InstanceRef string) (s3factory.S3Client, error) { + logger.Info(fmt.Sprintf("Resource refer to s3Instance: %s", ressourceS3InstanceRef)) + s3InstanceInfo := GetS3InstanceRefInfo(ressourceS3InstanceRef, ressourceNamespace) + s3Instance := &s3v1alpha1.S3Instance{} + err := client.Get(ctx, types.NamespacedName{Namespace: s3InstanceInfo.namespace, Name: s3InstanceInfo.name}, s3Instance) + if err != nil { + if k8sapierrors.IsNotFound(err) { + return nil, fmt.Errorf("S3Instance %s not found", s3InstanceInfo.name) + } + return nil, err + } + if !IsAllowedNamespaces(ressourceNamespace, s3Instance) { + logger.Info("resource %s try to use s3instance %s in namespace %s but is not allowed", ressourceName, s3InstanceInfo.name, s3InstanceInfo) + return nil, fmt.Errorf("S3Instance %s not found", s3InstanceInfo.name) + } + return GetS3ClientFromS3Instance(ctx, client, s3Instance) +} + +func GetS3ClientFromS3Instance(ctx context.Context, client client.Client, s3InstanceResource *s3v1alpha1.S3Instance) (s3factory.S3Client, error) { + + s3InstanceSecretSecret, err := getS3InstanceAccessSecret(ctx, client, s3InstanceResource) + if err != nil { + logger.Error(err, "Could not get s3Instance auth secret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretRef, "NamespacedName", s3InstanceResource.Namespace) + return nil, err + } + + s3InstanceCaCertSecret, err := getS3InstanceCaCertSecret(ctx, client, s3InstanceResource) + if err != nil { + logger.Error(err, "Could not get s3Instance cert secret in namespace", "s3InstanceSecretRefName", s3InstanceResource.Spec.SecretRef, "NamespacedName", s3InstanceResource.Namespace) + return nil, err + } + + allowedNamepaces := []string{s3InstanceResource.Namespace} + if len(s3InstanceResource.Spec.AllowedNamespaces) > 0 { + 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} + return s3factory.GenerateS3Client(s3Config.S3Provider, s3Config) +} + +func getS3InstanceAccessSecret(ctx context.Context, client client.Client, s3InstanceResource *s3v1alpha1.S3Instance) (corev1.Secret, error) { + s3InstanceSecret := &corev1.Secret{} + err := client.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 getS3InstanceCaCertSecret(ctx context.Context, client client.Client, s3InstanceResource *s3v1alpha1.S3Instance) (corev1.Secret, error) { + logger := log.FromContext(ctx) + + s3InstanceCaCertSecret := &corev1.Secret{} + + if s3InstanceResource.Spec.CaCertSecretRef == "" { + logger.Info("No CaCertSecretRef for s3instance %s", s3InstanceResource.Name) + return *s3InstanceCaCertSecret, nil + } + + err := client.Get(ctx, types.NamespacedName{Namespace: s3InstanceResource.Namespace, Name: s3InstanceResource.Spec.CaCertSecretRef}, s3InstanceCaCertSecret) + if err != nil { + 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 +} + +func GetS3InstanceRefInfo(ressourceS3InstanceRef string, ressourceNamespace string) S3InstanceInfo { + if strings.Contains(ressourceS3InstanceRef, "/") { + result := strings.Split(ressourceS3InstanceRef, "/") + return S3InstanceInfo{name: result[1], namespace: result[0]} + } + return S3InstanceInfo{name: ressourceS3InstanceRef, namespace: ressourceNamespace} +} + +func IsAllowedNamespaces(namespace string, s3Instance *s3v1alpha1.S3Instance) bool { + if s3Instance.Spec.AllowedNamespaces != nil && len(s3Instance.Spec.AllowedNamespaces) > 0 { + for _, allowedNamespace := range s3Instance.Spec.AllowedNamespaces { + 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 + } + } + } else { + return namespace == s3Instance.Namespace + } + return false +} + +type S3InstanceInfo struct { + name string + namespace string +} + +func (s3InstanceInfo S3InstanceInfo) String() string { + return fmt.Sprintf(s3InstanceInfo.namespace + "/" + s3InstanceInfo.name) +} diff --git a/controllers/s3/factory/interface.go b/internal/s3/factory/interface.go similarity index 74% rename from controllers/s3/factory/interface.go rename to internal/s3/factory/interface.go index e8bf10f..8690bb8 100644 --- a/controllers/s3/factory/interface.go +++ b/internal/s3/factory/interface.go @@ -1,4 +1,4 @@ -package factory +package s3factory import ( "fmt" @@ -35,25 +35,30 @@ type S3Client interface { GetUserPolicies(name string) ([]string, error) AddPoliciesToUser(accessKey string, policies []string) error RemovePoliciesFromUser(accessKey string, policies []string) error + GetConfig() *S3Config + ListBuckets() ([]string, error) } 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 GetS3Client(s3Provider string, S3Config *S3Config) (S3Client, error) { +func GenerateS3Client(s3Provider string, S3Config *S3Config) (S3Client, error) { if s3Provider == "mockedS3Provider" { return newMockedS3Client(), nil } if s3Provider == "minio" { - return newMinioS3Client(S3Config), nil + return newMinioS3Client(S3Config) } return nil, fmt.Errorf("s3 provider " + s3Provider + "not supported") } diff --git a/controllers/s3/factory/minioS3Client.go b/internal/s3/factory/minioS3Client.go similarity index 76% rename from controllers/s3/factory/minioS3Client.go rename to internal/s3/factory/minioS3Client.go index fb307d7..43600c2 100644 --- a/controllers/s3/factory/minioS3Client.go +++ b/internal/s3/factory/minioS3Client.go @@ -1,4 +1,4 @@ -package factory +package s3factory import ( "bytes" @@ -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,99 @@ 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, caCertificates []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(caCertificates) > 0 { + addTlsClientConfigToMinioOptions(caCertificates, 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, caCertificates []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(caCertificates) > 0 { + addTlsClientConfigToMinioOptions(caCertificates, 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(caCertificates []string, minioOptions *minio.Options) { + rootCAs, _ := x509.SystemCertPool() + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } - caCert, err := os.ReadFile(S3Config.CaBundlePath) - if err != nil { - s3Logger.Error(err, "an error occurred while reading a CA certificates bundle file") - } - rootCAs.AppendCertsFromPEM([]byte(caCert)) + for _, caCertificate := range caCertificates { + caCertificateAsByte := []byte(caCertificate) + caCertificateEncoded := base64.StdEncoding.EncodeToString(caCertificateAsByte) + rootCAs.AppendCertsFromPEM([]byte(caCertificateEncoded)) - minioOptions.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: rootCAs, - }, - } + } + + minioOptions.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: rootCAs, + }, } } @@ -118,6 +131,21 @@ func (minioS3Client *MinioS3Client) CreateBucket(name string) error { return minioS3Client.client.MakeBucket(context.Background(), name, minio.MakeBucketOptions{Region: minioS3Client.s3Config.Region}) } +func (minioS3Client *MinioS3Client) ListBuckets() ([]string, error) { + s3Logger.Info("listing bucket") + listBucketsInfo, err := minioS3Client.client.ListBuckets(context.Background()) + bucketsName := []string{} + if err != nil { + errAsResponse := minio.ToErrorResponse(err) + s3Logger.Error(err, "an error occurred while listing buckets", "code", errAsResponse.Code) + return bucketsName, err + } + for _, bucketInfo := range listBucketsInfo { + bucketsName = append(bucketsName, bucketInfo.Name) + } + return bucketsName, nil +} + // Will fail if bucket is not empty func (minioS3Client *MinioS3Client) DeleteBucket(name string) error { s3Logger.Info("deleting bucket", "bucket", name) @@ -325,17 +353,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) @@ -394,3 +417,7 @@ func (minioS3Client *MinioS3Client) AddPoliciesToUser(accessKey string, policies } return nil } + +func (minioS3Client *MinioS3Client) GetConfig() *S3Config { + return &minioS3Client.s3Config +} diff --git a/controllers/s3/factory/mockedS3Client.go b/internal/s3/factory/mockedS3Client.go similarity index 91% rename from controllers/s3/factory/mockedS3Client.go rename to internal/s3/factory/mockedS3Client.go index 96770fb..cfebeba 100644 --- a/controllers/s3/factory/mockedS3Client.go +++ b/internal/s3/factory/mockedS3Client.go @@ -1,10 +1,12 @@ -package factory +package s3factory import ( "github.com/minio/madmin-go/v3" ) -type MockedS3Client struct{} +type MockedS3Client struct { + s3Config S3Config +} func (mockedS3Provider *MockedS3Client) BucketExists(name string) (bool, error) { s3Logger.Info("checking bucket existence", "bucket", name) @@ -76,13 +78,11 @@ func (mockedS3Provider *MockedS3Client) PolicyExist(name string) (bool, error) { return true, nil } - func (mockedS3Provider *MockedS3Client) AddPoliciesToUser(username string, policies []string) error { s3Logger.Info("Adding policies to user", "user", username, "policies", policies) return nil } - func (mockedS3Provider *MockedS3Client) DeletePolicy(name string) error { s3Logger.Info("delete policy", "policy", name) return nil @@ -108,6 +108,14 @@ func (mockedS3Provider *MockedS3Client) RemovePoliciesFromUser(username string, return nil } +func (mockedS3Provider *MockedS3Client) ListBuckets() ([]string, error) { + return []string{}, nil +} + +func (mockedS3Provider *MockedS3Client) GetConfig() *S3Config { + return &mockedS3Provider.s3Config +} + func newMockedS3Client() *MockedS3Client { - return &MockedS3Client{} + return &MockedS3Client{s3Config: S3Config{}} } diff --git a/controllers/utils/password/password_generator.go b/internal/utils/password/password_generator.go similarity index 100% rename from controllers/utils/password/password_generator.go rename to internal/utils/password/password_generator.go diff --git a/controllers/utils/utils.go b/internal/utils/utils.go similarity index 99% rename from controllers/utils/utils.go rename to internal/utils/utils.go index 4f69b74..83c9eb2 100644 --- a/controllers/utils/utils.go +++ b/internal/utils/utils.go @@ -27,3 +27,5 @@ func UpdateConditions(existingConditions []metav1.Condition, newCondition metav1 return append([]metav1.Condition{newCondition}, existingConditions...) } + + diff --git a/main.go b/main.go index 9415abf..f496f4a 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. @@ -27,7 +28,6 @@ import ( s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" controllers "github.com/InseeFrLab/s3-operator/controllers" - "github.com/InseeFrLab/s3-operator/controllers/s3/factory" "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -70,18 +70,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 reconcilePeriod time.Duration //K8S related variable var overrideExistingSecret bool @@ -91,26 +80,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.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() @@ -150,51 +130,34 @@ func main() { os.Exit(1) } - // 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 - } - - // Creation of the S3 client - s3Config := &factory.S3Config{S3Provider: s3Provider, S3UrlEndpoint: s3EndpointUrl, Region: region, AccessKey: accessKeyFromEnvIfAvailable, SecretKey: secretKeyFromEnvIfAvailable, UseSsl: useSsl, CaCertificatesBase64: caCertificatesBase64, CaBundlePath: caCertificatesBundlePath} - s3Client, err := factory.GetS3Client(s3Config.S3Provider, s3Config) - 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", s3Client) + if err = (&controllers.S3InstanceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ReconcilePeriod: reconcilePeriod, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "S3Instance") os.Exit(1) } - if err = (&controllers.BucketReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - S3Client: s3Client, - BucketDeletion: bucketDeletion, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ReconcilePeriod: reconcilePeriod, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Bucket") os.Exit(1) } if err = (&controllers.PathReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - S3Client: s3Client, - PathDeletion: pathDeletion, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ReconcilePeriod: reconcilePeriod, }).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(), - S3Client: s3Client, - PolicyDeletion: policyDeletion, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ReconcilePeriod: reconcilePeriod, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Policy") os.Exit(1) @@ -202,13 +165,13 @@ func main() { if err = (&controllers.S3UserReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - S3Client: s3Client, - S3UserDeletion: s3userDeletion, OverrideExistingSecret: overrideExistingSecret, + ReconcilePeriod: reconcilePeriod, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "S3User") os.Exit(1) } + //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {