diff --git a/Dockerfile b/Dockerfile index 7f91fc3..b3785f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ RUN go mod download COPY cmd/main.go cmd/main.go COPY api/ api/ COPY internal/ internal/ +COPY pkg/ pkg/ # Build diff --git a/README.md b/README.md index 185ecbd..6cbded2 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,37 @@ spec: quota: default: 10000000 # override: 20000000 - + + # AccessPolicy to set on the bucket + # Default to Private. + # The type can be "Public", "Private" or "Custom" + # Public, anyone can access in rw without authentication. + # Private, only authenticated users with policies can access. + # Custom, define a policy to apply. + # In the example below, everybody can read, but only authenticated users with policy can write. + accessPolicy: + type: Custom + policyContent: |- + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": [ + "*" + ] + }, + "Action": [ + "s3:GetObject" + ], + "Resource": [ + "arn:aws:s3:::dummy-bucket/*" + ] + } + ] + } + # Optionnal, let empty if you have configured the default s3 else use an existing s3Instance s3InstanceRef: "s3-default-instance" diff --git a/api/v1alpha1/bucket_types.go b/api/v1alpha1/bucket_types.go index 578372f..d8dd49a 100644 --- a/api/v1alpha1/bucket_types.go +++ b/api/v1alpha1/bucket_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + s3model "github.com/InseeFrLab/s3-operator/pkg/s3/model" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -47,6 +48,10 @@ type BucketSpec struct { // Quota to apply to the bucket // +kubebuilder:validation:Required Quota Quota `json:"quota"` + + // AccessPolicy to apply to the bucket + // +kubebuilder:validation:Optional + AccessPolicy *AccessPolicy `json:"accessPolicy,omitempty"` } // BucketStatus defines the observed state of Bucket @@ -87,6 +92,18 @@ type Quota struct { Override int64 `json:"override,omitempty"` } +type AccessPolicy struct { + // type of the AccessPolicy + // +kubebuilder:validation:Required + // +kubebuilder:validation:Default=Private + // +kubebuilder:validation:Enum=Private;Public;Custom + Type s3model.BucketAccessPolicyType `json:"type"` + + // Content of the policy (IAM JSON format) for Custom AccessPolicy + // +kubebuilder:validation:Optional + PolicyContent string `json:"policyContent,omitempty"` +} + func init() { SchemeBuilder.Register(&Bucket{}, &BucketList{}) } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f191f44..9ef4a2f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,21 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessPolicy) DeepCopyInto(out *AccessPolicy) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessPolicy. +func (in *AccessPolicy) DeepCopy() *AccessPolicy { + if in == nil { + return nil + } + out := new(AccessPolicy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Bucket) DeepCopyInto(out *Bucket) { *out = *in @@ -93,6 +108,11 @@ func (in *BucketSpec) DeepCopyInto(out *BucketSpec) { copy(*out, *in) } out.Quota = in.Quota + if in.AccessPolicy != nil { + in, out := &in.AccessPolicy, &out.AccessPolicy + *out = new(AccessPolicy) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BucketSpec. diff --git a/cmd/main.go b/cmd/main.go index 37c86fb..d918428 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -33,7 +33,7 @@ import ( s3InstanceControllers "github.com/InseeFrLab/s3-operator/internal/controller/s3instance" userControllers "github.com/InseeFrLab/s3-operator/internal/controller/user" "github.com/InseeFrLab/s3-operator/internal/helpers" - s3factory "github.com/InseeFrLab/s3-operator/internal/s3/factory/impl" + s3factory "github.com/InseeFrLab/s3-operator/pkg/s3/factory/impl" "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/runtime" diff --git a/config/crd/bases/s3.onyxia.sh_buckets.yaml b/config/crd/bases/s3.onyxia.sh_buckets.yaml index 4d46e44..af1e2c7 100644 --- a/config/crd/bases/s3.onyxia.sh_buckets.yaml +++ b/config/crd/bases/s3.onyxia.sh_buckets.yaml @@ -39,6 +39,23 @@ spec: spec: description: BucketSpec defines the desired state of Bucket properties: + accessPolicy: + description: AccessPolicy to apply to the bucket + properties: + policyContent: + description: Content of the policy (IAM JSON format) for Custom + AccessPolicy + type: string + type: + description: type of the AccessPolicy + enum: + - Private + - Public + - Custom + type: string + required: + - type + type: object name: description: Name of the bucket type: string diff --git a/deploy/charts/s3-operator/Chart.yaml b/deploy/charts/s3-operator/Chart.yaml index 6653750..0970624 100644 --- a/deploy/charts/s3-operator/Chart.yaml +++ b/deploy/charts/s3-operator/Chart.yaml @@ -13,9 +13,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.7.0 +version: 0.8.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.12.0" +appVersion: "v0.13.0" diff --git a/deploy/charts/s3-operator/templates/crds/buckets.yaml b/deploy/charts/s3-operator/templates/crds/buckets.yaml index 7c510f9..b65b177 100644 --- a/deploy/charts/s3-operator/templates/crds/buckets.yaml +++ b/deploy/charts/s3-operator/templates/crds/buckets.yaml @@ -6,7 +6,7 @@ metadata: {{- if .Values.crds.keep }} helm.sh/resource-policy: keep {{- end }} - controller-gen.kubebuilder.io/version: v0.11.1 + controller-gen.kubebuilder.io/version: v0.17.1 labels: {{- include "s3-operator.labels" . | nindent 4 }} name: buckets.s3.onyxia.sh @@ -25,20 +25,42 @@ spec: description: Bucket is the Schema for the buckets 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' + 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' + 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: BucketSpec defines the desired state of Bucket properties: + accessPolicy: + description: AccessPolicy to apply to the bucket + properties: + policyContent: + description: Content of the policy (IAM JSON format) for Custom + AccessPolicy + type: string + type: + description: type of the AccessPolicy + enum: + - Private + - Public + - Custom + type: string + required: + - type + type: object name: description: Name of the bucket type: string @@ -80,45 +102,39 @@ spec: description: BucketStatus defines the observed state of Bucket 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' + 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 }" + description: Condition contains details for one aspect of the current + state of this API Resource. 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. + 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. + 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. + 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. + 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 @@ -133,10 +149,6 @@ spec: 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 diff --git a/deploy/charts/s3-operator/templates/crds/paths.yaml b/deploy/charts/s3-operator/templates/crds/paths.yaml index f2c6219..798c247 100644 --- a/deploy/charts/s3-operator/templates/crds/paths.yaml +++ b/deploy/charts/s3-operator/templates/crds/paths.yaml @@ -6,7 +6,7 @@ metadata: {{- if .Values.crds.keep }} helm.sh/resource-policy: keep {{- end }} - controller-gen.kubebuilder.io/version: v0.11.1 + controller-gen.kubebuilder.io/version: v0.17.1 labels: {{- include "s3-operator.labels" . | nindent 4 }} name: paths.s3.onyxia.sh @@ -25,14 +25,19 @@ spec: description: Path is the Schema for the paths 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' + 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' + 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 @@ -64,45 +69,39 @@ spec: description: PathStatus defines the observed state of Path 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' + 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 }" + description: Condition contains details for one aspect of the current + state of this API Resource. 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. + 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. + 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. + 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. + 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 @@ -117,10 +116,6 @@ spec: 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 diff --git a/deploy/charts/s3-operator/templates/crds/policies.yaml b/deploy/charts/s3-operator/templates/crds/policies.yaml index dec5a83..7a99751 100644 --- a/deploy/charts/s3-operator/templates/crds/policies.yaml +++ b/deploy/charts/s3-operator/templates/crds/policies.yaml @@ -6,7 +6,7 @@ metadata: {{- if .Values.crds.keep }} helm.sh/resource-policy: keep {{- end }} - controller-gen.kubebuilder.io/version: v0.11.1 + controller-gen.kubebuilder.io/version: v0.17.1 labels: {{- include "s3-operator.labels" . | nindent 4 }} name: policies.s3.onyxia.sh @@ -25,14 +25,19 @@ spec: description: Policy is the Schema for the policies 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' + 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' + 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 @@ -63,45 +68,39 @@ spec: description: PolicyStatus defines the observed state of Policy 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' + 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 }" + description: Condition contains details for one aspect of the current + state of this API Resource. 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. + 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. + 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. + 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. + 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 @@ -116,10 +115,6 @@ spec: 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 diff --git a/deploy/charts/s3-operator/templates/crds/s3instances.yaml b/deploy/charts/s3-operator/templates/crds/s3instances.yaml index a2354bc..96ff00e 100644 --- a/deploy/charts/s3-operator/templates/crds/s3instances.yaml +++ b/deploy/charts/s3-operator/templates/crds/s3instances.yaml @@ -6,7 +6,7 @@ metadata: {{- if .Values.crds.keep }} helm.sh/resource-policy: keep {{- end }} - controller-gen.kubebuilder.io/version: v0.11.1 + controller-gen.kubebuilder.io/version: v0.17.1 labels: {{- include "s3-operator.labels" . | nindent 4 }} name: s3instances.s3.onyxia.sh @@ -25,14 +25,19 @@ spec: 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' + 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' + 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 @@ -91,11 +96,7 @@ spec: description: url of the S3Instance type: string required: - - bucketDeletionEnabled - - pathDeletionEnabled - - policyDeletionEnabled - s3Provider - - s3UserDeletionEnabled - secretRef - url type: object @@ -103,45 +104,39 @@ spec: 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' + 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 }" + description: Condition contains details for one aspect of the current + state of this API Resource. 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. + 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. + 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. + 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. + 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 @@ -156,10 +151,6 @@ spec: 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 diff --git a/deploy/charts/s3-operator/templates/crds/s3users.yaml b/deploy/charts/s3-operator/templates/crds/s3users.yaml index 7893652..1acf293 100644 --- a/deploy/charts/s3-operator/templates/crds/s3users.yaml +++ b/deploy/charts/s3-operator/templates/crds/s3users.yaml @@ -6,7 +6,7 @@ metadata: {{- if .Values.crds.keep }} helm.sh/resource-policy: keep {{- end }} - controller-gen.kubebuilder.io/version: v0.11.1 + controller-gen.kubebuilder.io/version: v0.17.1 labels: {{- include "s3-operator.labels" . | nindent 4 }} name: s3users.s3.onyxia.sh @@ -25,14 +25,19 @@ spec: description: S3User is the Schema for the S3Users 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' + 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' + 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 @@ -57,6 +62,18 @@ spec: x-kubernetes-validations: - message: s3InstanceRef is immutable rule: self == oldSelf + secretFieldNameAccessKey: + default: accessKey + description: |- + SecretFieldNameAccessKey associated to the S3User + Allow overridden the default key to store the accessKey value in the secret + type: string + secretFieldNameSecretKey: + default: secretKey + description: |- + SecretFieldNameSecretKey associated to the S3User + Allow overridden the default key to store the secretKey value in the secret + type: string secretName: description: SecretName associated to the S3User type: string @@ -67,45 +84,39 @@ spec: description: S3UserStatus defines the observed state of S3User 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' + 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 }" + description: Condition contains details for one aspect of the current + state of this API Resource. 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. + 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. + 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. + 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. + 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 @@ -120,10 +131,6 @@ spec: 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 diff --git a/deploy/charts/s3-operator/templates/default-s3instance.yaml b/deploy/charts/s3-operator/templates/default-s3instance.yaml index 26a10ca..24fd93a 100644 --- a/deploy/charts/s3-operator/templates/default-s3instance.yaml +++ b/deploy/charts/s3-operator/templates/default-s3instance.yaml @@ -1,4 +1,10 @@ {{- if .Values.s3.default.enabled -}} +--- +apiVersion: v1 +kind: Namespace +metadata: + name: s3-operator +--- apiVersion: s3.onyxia.sh/v1alpha1 kind: S3Instance metadata: @@ -9,6 +15,7 @@ metadata: control-plane: controller-manager {{- include "s3-operator.labels" . | nindent 4 }} name: default + namespace: s3-operator spec: s3Provider: {{ .Values.s3.default.s3Provider }} url: {{ .Values.s3.default.url }} @@ -19,11 +26,12 @@ spec: {{- end }} {{- if .Values.s3.default.caCertSecretRef }} caCertSecretRef: {{ .Values.s3.default.caCertSecretRef }} - {{- else }} - caCertSecretRef: default-s3instance-certificates {{- end }} {{- if .Values.s3.default.allowedNamespaces }} - allowedNamespaces: {{ .Values.s3.default.allowedNamespaces }} + allowedNamespaces: + {{- range .Values.s3.default.allowedNamespaces }} + - {{ . | quote }} + {{- end }} {{- end }} {{- if .Values.s3.default.region }} region: {{ .Values.s3.default.region }} @@ -46,11 +54,15 @@ metadata: {{- include "s3-operator.labels" . | nindent 4 }} name: default-s3instance-credentials type: Opaque -data: - S3_ACCESS_KEY: {{- .Values.s3.default.accessKey }} - S3_SECRET_KEY: {{- .Values.s3.default.secretKey }} +stringData: + S3_ACCESS_KEY: {{ .Values.s3.default.accessKey }} + S3_SECRET_KEY: {{ .Values.s3.default.secretKey }} {{- end }} -{{- if not .Values.s3.default.caCertSecretRef }} + +{{- /* +The secret is created when the User doesn't have set a caCertSecretRef, but filled caCertificatesBase64. +*/}} +{{- if and (not .Values.s3.default.caCertSecretRef) .Values.s3.default.caCertificatesBase64 }} --- apiVersion: v1 kind: Secret @@ -64,6 +76,6 @@ metadata: name: default-s3instance-certificates type: Opaque data: - ca.crt: {{- .Values.s3.default.caCertificatesBase64 }} + ca.crt: {{ .Values.s3.default.caCertificatesBase64 }} {{- end }} {{- end -}} \ No newline at end of file diff --git a/deploy/charts/s3-operator/templates/manager-rbac.yaml b/deploy/charts/s3-operator/templates/manager-rbac.yaml index 6349b24..599c4e1 100644 --- a/deploy/charts/s3-operator/templates/manager-rbac.yaml +++ b/deploy/charts/s3-operator/templates/manager-rbac.yaml @@ -5,7 +5,6 @@ metadata: labels: {{- include "s3-operator.labels" . | nindent 4 }} rules: -rules: - apiGroups: - "" resources: diff --git a/deploy/charts/s3-operator/values.yaml b/deploy/charts/s3-operator/values.yaml index 84abf40..590104a 100644 --- a/deploy/charts/s3-operator/values.yaml +++ b/deploy/charts/s3-operator/values.yaml @@ -38,7 +38,7 @@ s3: url: "https://localhost:9000" accessKey: "accessKey" secretKey: "secretKey" - caCertificatesBase64: base64encodedPEMFormatCACertificate + # caCertificatesBase64: "" region: us-east-1 # secretRef: "my-s3-operator-auth-secret" # caCertSecretRef: "my-s3-operator-cert-secret" diff --git a/go.mod b/go.mod index 70381ce..e4f4ad4 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,10 @@ require ( github.com/minio/minio-go/v7 v7.0.84 github.com/onsi/ginkgo/v2 v2.22.2 github.com/onsi/gomega v1.36.2 + github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 + gotest.tools/v3 v3.0.3 k8s.io/api v0.32.0 k8s.io/apimachinery v0.32.0 k8s.io/client-go v0.32.0 @@ -60,7 +62,6 @@ require ( 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.3-0.20240916144458-20a13a1f6b7c // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_golang v1.20.4 // indirect diff --git a/go.sum b/go.sum index c2f0df3..6946216 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,7 @@ github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 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.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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= @@ -100,6 +101,7 @@ github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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= @@ -133,6 +135,7 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt 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.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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= @@ -167,6 +170,7 @@ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 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/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-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= @@ -200,6 +204,7 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.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-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 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= @@ -222,6 +227,8 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0= diff --git a/internal/controller/bucket/controller.go b/internal/controller/bucket/controller.go index 593f4e9..f7960a2 100644 --- a/internal/controller/bucket/controller.go +++ b/internal/controller/bucket/controller.go @@ -23,7 +23,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "github.com/InseeFrLab/s3-operator/internal/helpers" - s3factory "github.com/InseeFrLab/s3-operator/internal/s3/factory" + s3factory "github.com/InseeFrLab/s3-operator/pkg/s3/factory" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" diff --git a/internal/controller/bucket/finalizer_test.go b/internal/controller/bucket/finalizer_test.go index 5ccfb22..c562dd0 100644 --- a/internal/controller/bucket/finalizer_test.go +++ b/internal/controller/bucket/finalizer_test.go @@ -51,6 +51,7 @@ func TestHandleDelete(t *testing.T) { Name: "example-bucket", S3InstanceRef: "s3-operator/default", Quota: s3v1alpha1.Quota{Default: 10}, + AccessPolicy: nil, }, } diff --git a/internal/controller/bucket/reconcile.go b/internal/controller/bucket/reconcile.go index 75e140c..5cce542 100644 --- a/internal/controller/bucket/reconcile.go +++ b/internal/controller/bucket/reconcile.go @@ -19,9 +19,8 @@ package bucket_controller import ( "context" "fmt" - s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" - + s3model "github.com/InseeFrLab/s3-operator/pkg/s3/model" k8sapierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -362,6 +361,93 @@ func (r *BucketReconciler) handleUpdate( } } + // Doing nothing if the bucket manifest have no access policy defined + if bucketResource.Spec.AccessPolicy != nil { + accessPolicyCfg := bucketResource.Spec.AccessPolicy + + // getting current access policy to check if the policy is the same as configure or not + currentBucketAccessPolicy, err := s3Client.GetBucketAccessPolicy(bucketResource.Spec.Name) + if err != nil { + logger.Error( + err, + "An error occurred while getting the access policy for bucket ressource", + "policyType", + accessPolicyCfg.Type, + "bucketName", + bucketResource.Spec.Name, + "NamespacedName", + req.NamespacedName.String(), + ) + return r.SetReconciledCondition( + ctx, + req, + bucketResource, + s3v1alpha1.Unreachable, + fmt.Sprintf( + "Getting the current access policy of bucket [%s] has failed", + bucketResource.Spec.Name, + ), + err, + ) + } + + wantedBucketAccessPolicy, err := s3model.NewBucketAccessPolicy(bucketResource.Spec.Name, accessPolicyCfg.Type, accessPolicyCfg.PolicyContent) + if err != nil { + logger.Error( + err, + "An error occurred while init the access policy to configure for bucket ressource", + "policyType", + accessPolicyCfg.Type, + "bucketName", + bucketResource.Spec.Name, + "NamespacedName", + req.NamespacedName.String(), + ) + return r.SetReconciledCondition( + ctx, + req, + bucketResource, + s3v1alpha1.Unreachable, + fmt.Sprintf( + "Setting an access policy of type [%s] on bucket [%s] has failed", + accessPolicyCfg.Type, + bucketResource.Spec.Name, + ), + err, + ) + } + + // check if policy content are equals + if !currentBucketAccessPolicy.Equal(wantedBucketAccessPolicy) { + // policies are not equal, update required + err = s3Client.SetBucketAccessPolicy(bucketResource.Spec.Name, accessPolicyCfg.Type, accessPolicyCfg.PolicyContent) + if err != nil { + logger.Error( + err, + "An error occurred while setting the access policy for bucket ressource", + "policyType", + accessPolicyCfg.Type, + "bucketName", + bucketResource.Spec.Name, + "NamespacedName", + req.NamespacedName.String(), + ) + return r.SetReconciledCondition( + ctx, + req, + bucketResource, + s3v1alpha1.Unreachable, + fmt.Sprintf( + "Setting an access policy of type [%s] on bucket [%s] has failed", + accessPolicyCfg.Type, + bucketResource.Spec.Name, + ), + err, + ) + } + } + } + return r.SetReconciledCondition( ctx, req, @@ -477,6 +563,35 @@ func (r *BucketReconciler) handleCreation( } } + // Set accessPolicy + if bucketResource.Spec.AccessPolicy != nil { + err = s3Client.SetBucketAccessPolicy(bucketResource.Spec.Name, bucketResource.Spec.AccessPolicy.Type, bucketResource.Spec.AccessPolicy.PolicyContent) + if err != nil { + logger.Error( + err, + "An error occurred while setting the access policy for bucket ressource", + "policyType", + bucketResource.Spec.AccessPolicy.Type, + "bucketName", + bucketResource.Spec.Name, + "NamespacedName", + req.NamespacedName.String(), + ) + return r.SetReconciledCondition( + ctx, + req, + bucketResource, + s3v1alpha1.Unreachable, + fmt.Sprintf( + "Setting an access policy of type [%s] on bucket [%s] has failed", + bucketResource.Spec.AccessPolicy.Type, + bucketResource.Spec.Name, + ), + err, + ) + } + } + return r.SetReconciledCondition( ctx, req, diff --git a/internal/controller/path/controller.go b/internal/controller/path/controller.go index 8c80136..b8fd17f 100644 --- a/internal/controller/path/controller.go +++ b/internal/controller/path/controller.go @@ -20,7 +20,7 @@ import ( "time" "github.com/InseeFrLab/s3-operator/internal/helpers" - s3factory "github.com/InseeFrLab/s3-operator/internal/s3/factory" + s3factory "github.com/InseeFrLab/s3-operator/pkg/s3/factory" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/internal/controller/policy/controller.go b/internal/controller/policy/controller.go index 01be910..213fd5d 100644 --- a/internal/controller/policy/controller.go +++ b/internal/controller/policy/controller.go @@ -20,7 +20,7 @@ import ( "time" "github.com/InseeFrLab/s3-operator/internal/helpers" - s3factory "github.com/InseeFrLab/s3-operator/internal/s3/factory" + s3factory "github.com/InseeFrLab/s3-operator/pkg/s3/factory" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/internal/controller/s3instance/controller.go b/internal/controller/s3instance/controller.go index bbd8fc2..e63b345 100644 --- a/internal/controller/s3instance/controller.go +++ b/internal/controller/s3instance/controller.go @@ -21,7 +21,7 @@ import ( s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" "github.com/InseeFrLab/s3-operator/internal/helpers" - s3factory "github.com/InseeFrLab/s3-operator/internal/s3/factory" + s3factory "github.com/InseeFrLab/s3-operator/pkg/s3/factory" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/internal/controller/user/controller.go b/internal/controller/user/controller.go index 717ceb4..0eb3d73 100644 --- a/internal/controller/user/controller.go +++ b/internal/controller/user/controller.go @@ -20,7 +20,7 @@ import ( "time" "github.com/InseeFrLab/s3-operator/internal/helpers" - s3factory "github.com/InseeFrLab/s3-operator/internal/s3/factory" + s3factory "github.com/InseeFrLab/s3-operator/pkg/s3/factory" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" diff --git a/internal/helpers/s3instance.go b/internal/helpers/s3instance.go index 31e83c1..cae983f 100644 --- a/internal/helpers/s3instance.go +++ b/internal/helpers/s3instance.go @@ -21,11 +21,11 @@ import ( "fmt" "strings" - s3client "github.com/InseeFrLab/s3-operator/internal/s3/client" corev1 "k8s.io/api/core/v1" s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" - s3factory "github.com/InseeFrLab/s3-operator/internal/s3/factory" + "github.com/InseeFrLab/s3-operator/pkg/s3/client" + "github.com/InseeFrLab/s3-operator/pkg/s3/factory" k8sapierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -97,17 +97,21 @@ func (s3InstanceHelper *S3InstanceHelper) GetS3ClientFromS3Instance( return nil, err } - s3InstanceCaCertSecret, err := s3InstanceHelper.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 + var s3InstanceCaCertificates []string + if s3InstanceResource.Spec.CaCertSecretRef != "" { + s3InstanceCaCertSecret, err := s3InstanceHelper.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 + } + s3InstanceCaCertificates = []string{string(s3InstanceCaCertSecret.Data["ca.crt"])} } allowedNamepaces := []string{s3InstanceResource.Namespace} @@ -122,7 +126,7 @@ func (s3InstanceHelper *S3InstanceHelper) GetS3ClientFromS3Instance( S3Url: s3InstanceResource.Spec.Url, Region: s3InstanceResource.Spec.Region, AllowedNamespaces: allowedNamepaces, - CaCertificatesBase64: []string{string(s3InstanceCaCertSecret.Data["ca.crt"])}, + CaCertificatesBase64: s3InstanceCaCertificates, BucketDeletionEnabled: s3InstanceResource.Spec.BucketDeletionEnabled, S3UserDeletionEnabled: s3InstanceResource.Spec.S3UserDeletionEnabled, PolicyDeletionEnabled: s3InstanceResource.Spec.PolicyDeletionEnabled, diff --git a/internal/s3/client/impl/minioS3Client.go b/pkg/s3/client/impl/minioS3Client.go similarity index 87% rename from internal/s3/client/impl/minioS3Client.go rename to pkg/s3/client/impl/minioS3Client.go index c340fbd..37de272 100644 --- a/internal/s3/client/impl/minioS3Client.go +++ b/pkg/s3/client/impl/minioS3Client.go @@ -26,7 +26,8 @@ import ( neturl "net/url" "strings" - s3client "github.com/InseeFrLab/s3-operator/internal/s3/client" + s3client "github.com/InseeFrLab/s3-operator/pkg/s3/client" + s3model "github.com/InseeFrLab/s3-operator/pkg/s3/model" "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" @@ -74,7 +75,7 @@ func generateMinioClient( caCertificates []string, ) (*minio.Client, error) { s3Logger := ctrl.Log.WithValues("logger", "s3clientimplminio") - hostname, isSSL, err := extractHostAndScheme(url) + endpoint, isSSL, err := constructEndpointFromURL(url) if err != nil { s3Logger.Error(err, "an error occurred while creating a new minio client") return nil, err @@ -90,7 +91,7 @@ func generateMinioClient( addTlsClientConfigToMinioOptions(caCertificates, minioOptions) } - minioClient, err := minio.New(hostname, minioOptions) + minioClient, err := minio.New(endpoint, minioOptions) if err != nil { s3Logger.Error(err, "an error occurred while creating a new minio client") return nil, err @@ -105,7 +106,7 @@ func generateAdminMinioClient( caCertificates []string, ) (*madmin.AdminClient, error) { s3Logger := ctrl.Log.WithValues("logger", "s3clientimplminio") - hostname, isSSL, err := extractHostAndScheme(url) + endpoint, isSSL, err := constructEndpointFromURL(url) if err != nil { s3Logger.Error(err, "an error occurred while creating a new minio admin client") return nil, err @@ -120,7 +121,7 @@ func generateAdminMinioClient( addTlsClientConfigToMinioAdminOptions(caCertificates, minioOptions) } - minioAdminClient, err := madmin.NewWithOptions(hostname, minioOptions) + minioAdminClient, err := madmin.NewWithOptions(endpoint, minioOptions) if err != nil { s3Logger.Error(err, "an error occurred while creating a new minio admin client") return nil, err @@ -129,12 +130,19 @@ func generateAdminMinioClient( return minioAdminClient, nil } -func extractHostAndScheme(url string) (string, bool, error) { +func constructEndpointFromURL(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 + + var endpoint = parsedURL.Hostname() + if !((parsedURL.Scheme == "https" && parsedURL.Port() == "443") || + (parsedURL.Scheme == "http" && parsedURL.Port() == "80")) { + endpoint = fmt.Sprintf("%s:%s", endpoint, parsedURL.Port()) + } + + return endpoint, parsedURL.Scheme == "https", nil } func addTlsClientConfigToMinioOptions(caCertificates []string, minioOptions *minio.Options) { @@ -289,6 +297,64 @@ func (minioS3Client *MinioS3Client) DeletePath(bucketname string, path string) e return nil } +func (minioS3Client *MinioS3Client) GetBucketAccessPolicy(bucketname string) (*s3model.BucketAccessPolicy, error) { + s3Logger := ctrl.Log.WithValues("logger", "s3clientimplminio") + s3Logger.Info("setting the access policy on bucket", "bucket", bucketname) + + policy, err := minioS3Client.client.GetBucketPolicy( + context.Background(), + bucketname, + ) + if err != nil { + s3Logger.Error( + err, + "an error occurred when getting the bucket policy", + "bucket", + bucketname, + ) + return nil, err + } + + bucketAccessPolicy, err := s3model.LoadBucketAccessPolicy(bucketname, policy) + if err != nil { + return nil, fmt.Errorf("failed to load bucket access policy: %v", err) + } + return bucketAccessPolicy, nil +} + +func (minioS3Client *MinioS3Client) SetBucketAccessPolicy(bucketname string, accessPolicyType s3model.BucketAccessPolicyType, accessPolicy string) error { + s3Logger := ctrl.Log.WithValues("logger", "s3clientimplminio") + s3Logger.Info("setting the access policy on bucket", "bucket", bucketname, "policyType", accessPolicyType) + + bucketAccessPolicy, err := s3model.NewBucketAccessPolicy(bucketname, accessPolicyType, accessPolicy) + if err != nil { + return fmt.Errorf("failed to create bucket access policy object: %v", err) + } + + jsonContent, err := bucketAccessPolicy.Content.JSON() + if err != nil { + return fmt.Errorf("failed to marshal bucket access policy object: %v", err) + } + + err = minioS3Client.client.SetBucketPolicy( + context.Background(), + bucketname, + string(jsonContent), + ) + if err != nil { + s3Logger.Error( + err, + "an error occurred during path deletion on bucket", + "bucket", + bucketname, + "policyType", + accessPolicyType, + ) + return err + } + return nil +} + // ///////////////// // Quota methods // // ///////////////// diff --git a/internal/s3/client/impl/mockedS3Client.go b/pkg/s3/client/impl/mockedS3Client.go similarity index 88% rename from internal/s3/client/impl/mockedS3Client.go rename to pkg/s3/client/impl/mockedS3Client.go index ea79403..517ac78 100644 --- a/internal/s3/client/impl/mockedS3Client.go +++ b/pkg/s3/client/impl/mockedS3Client.go @@ -17,7 +17,8 @@ limitations under the License. package s3clientimpl import ( - s3client "github.com/InseeFrLab/s3-operator/internal/s3/client" + s3client "github.com/InseeFrLab/s3-operator/pkg/s3/client" + s3model "github.com/InseeFrLab/s3-operator/pkg/s3/model" "github.com/minio/madmin-go/v3" ctrl "sigs.k8s.io/controller-runtime" ) @@ -44,6 +45,18 @@ func (mockedS3Provider *MockedS3Client) DeleteBucket(name string) error { return nil } +func (mockedS3Provider *MockedS3Client) GetBucketAccessPolicy(bucketname string) (*s3model.BucketAccessPolicy, error) { + s3Logger := ctrl.Log.WithValues("logger", "s3ClientImplMocked") + s3Logger.Info("getting the access policy of a bucket", "bucket", bucketname) + return nil, nil +} + +func (mockedS3Provider *MockedS3Client) SetBucketAccessPolicy(bucketname string, accessPolicyType s3model.BucketAccessPolicyType, accessPolicy string) error { + s3Logger := ctrl.Log.WithValues("logger", "s3ClientImplMocked") + s3Logger.Info("setting the access policy of a bucket", "bucket", bucketname, "policy", accessPolicyType) + return nil +} + func (mockedS3Provider *MockedS3Client) CreatePath(bucketname string, path string) error { s3Logger := ctrl.Log.WithValues("logger", "s3ClientImplMocked") s3Logger.Info("creating a path on a bucket", "bucket", bucketname, "path", path) diff --git a/internal/s3/client/s3client.go b/pkg/s3/client/s3client.go similarity index 89% rename from internal/s3/client/s3client.go rename to pkg/s3/client/s3client.go index cc48515..0f6991c 100644 --- a/internal/s3/client/s3client.go +++ b/pkg/s3/client/s3client.go @@ -17,6 +17,7 @@ limitations under the License. package s3client import ( + s3model "github.com/InseeFrLab/s3-operator/pkg/s3/model" "github.com/minio/madmin-go/v3" ) @@ -36,6 +37,8 @@ type S3Config struct { type S3Client interface { BucketExists(name string) (bool, error) + GetBucketAccessPolicy(bucketname string) (*s3model.BucketAccessPolicy, error) + SetBucketAccessPolicy(bucketname string, accessPolicyType s3model.BucketAccessPolicyType, accessPolicy string) error CreateBucket(name string) error DeleteBucket(name string) error CreatePath(bucketname string, path string) error diff --git a/internal/s3/factory/impl/s3factoryImpl.go b/pkg/s3/factory/impl/s3factoryImpl.go similarity index 88% rename from internal/s3/factory/impl/s3factoryImpl.go rename to pkg/s3/factory/impl/s3factoryImpl.go index 8cca6fe..2122fce 100644 --- a/internal/s3/factory/impl/s3factoryImpl.go +++ b/pkg/s3/factory/impl/s3factoryImpl.go @@ -19,8 +19,8 @@ package s3factory import ( "fmt" - s3client "github.com/InseeFrLab/s3-operator/internal/s3/client" - s3clientImpl "github.com/InseeFrLab/s3-operator/internal/s3/client/impl" + s3client "github.com/InseeFrLab/s3-operator/pkg/s3/client" + s3clientImpl "github.com/InseeFrLab/s3-operator/pkg/s3/client/impl" ) type S3Factory struct { diff --git a/internal/s3/factory/s3factory.go b/pkg/s3/factory/s3factory.go similarity index 91% rename from internal/s3/factory/s3factory.go rename to pkg/s3/factory/s3factory.go index 2042b4f..ce65f6c 100644 --- a/internal/s3/factory/s3factory.go +++ b/pkg/s3/factory/s3factory.go @@ -17,7 +17,7 @@ limitations under the License. package s3factory import ( - s3client "github.com/InseeFrLab/s3-operator/internal/s3/client" + s3client "github.com/InseeFrLab/s3-operator/pkg/s3/client" ) type S3Factory interface { diff --git a/pkg/s3/model/bucket_access_policy.go b/pkg/s3/model/bucket_access_policy.go new file mode 100644 index 0000000..6f17edd --- /dev/null +++ b/pkg/s3/model/bucket_access_policy.go @@ -0,0 +1,240 @@ +package model + +import ( + "encoding/json" + "fmt" + "github.com/pkg/errors" + "reflect" + "slices" + "strings" +) + +const ( + // PrivateBucketAccessPolicy only authenticated users can interact with Bucket + PrivateBucketAccessPolicy BucketAccessPolicyType = "Private" + // PublicBucketAccessPolicy all users, included anonymous have access to Bucket + PublicBucketAccessPolicy BucketAccessPolicyType = "Public" + // CustomBucketAccessPolicy custom policy + CustomBucketAccessPolicy BucketAccessPolicyType = "Custom" +) + +var ( + // ErrUnknownBucketAccessPolicyType is raised when the given policy type doesn't exist or is not implemented + ErrUnknownBucketAccessPolicyType = "unknown bucket access policy type" + // ErrInvalidBucketAccessPolicy is raised when the given policy can't be parsed + ErrInvalidBucketAccessPolicy = "invalid bucket access policy" + // ErrLoadingBucketAccessPolicy is raised when the load of the current policy failed + ErrLoadingBucketAccessPolicy = "failed to load the current policy" +) + +// BucketAccessPolicyType represents the type +// of the BucketAccessPolicy +type BucketAccessPolicyType string + +// BucketAccessPolicy represents the BucketAccessPolicy +type BucketAccessPolicy struct { + Type BucketAccessPolicyType + Content BucketAccessPolicyContent +} + +// BucketAccessPolicyContent wrap an IAM object represents +// the policy content +type BucketAccessPolicyContent struct { + IAM +} + +// NewBucketAccessPolicy return an already defined policy for PRIVATE and PUBLIC type and a CUSTOM for custom policy +func NewBucketAccessPolicy(bucketName string, policyType BucketAccessPolicyType, policy string) (*BucketAccessPolicy, error) { + var ( + accessPolicy *BucketAccessPolicy + err error + ) + + switch policyType { + case PrivateBucketAccessPolicy: + accessPolicy = privateBucketAccessPolicy() + case PublicBucketAccessPolicy: + accessPolicy = publicBucketAccessPolicy(bucketName) + case CustomBucketAccessPolicy: + if policy == "" { + return nil, fmt.Errorf(ErrInvalidBucketAccessPolicy) + } + + accessPolicy, err = customBucketAccessPolicy(policy) + if err != nil { + return accessPolicy, nil + } + default: + return accessPolicy, errors.New(ErrUnknownBucketAccessPolicyType) + } + + return accessPolicy, nil +} + +// LoadBucketAccessPolicy construct the BucketAccessPolicy object from policy given as string +func LoadBucketAccessPolicy(bucketName string, policy string) (*BucketAccessPolicy, error) { + accessPolicy := &BucketAccessPolicy{} + + // when a bucket is created is by default Private + // without any policy set + if policy == "" { + accessPolicy.Type = PrivateBucketAccessPolicy + return accessPolicy, nil + } + + err := json.Unmarshal([]byte(policy), &accessPolicy.Content) + if err != nil { + return nil, errors.Wrap(err, ErrLoadingBucketAccessPolicy) + } + + accessPolicy.Type = findAccessBucketPolicyTypeFromContent(bucketName, accessPolicy.Content) + + return accessPolicy, nil +} + +// JSON convert BucketAccessPolicyContent to JSON +func (b BucketAccessPolicyContent) JSON() ([]byte, error) { + return json.Marshal(b) +} + +func (b *BucketAccessPolicy) Sort() { + for _, stmt := range b.Content.Statement { + for _, principal := range stmt.Principal { + slices.Sort(principal) + } + + slices.Sort(stmt.Resource) + slices.Sort(stmt.Action) + } +} + +func (b *BucketAccessPolicy) Equal(compare *BucketAccessPolicy) bool { + if b == nil && compare == nil { + return true + } + + if b == nil || compare == nil { + return false + } + + // check type + if b.Type != compare.Type { + return false + } + + b.Sort() + compare.Sort() + + return reflect.DeepEqual(compare.Content, b.Content) +} + +func (b BucketAccessPolicyContent) isPublic(bucket string) bool { + for _, stmt := range b.Statement { + for _, principal := range stmt.Principal { + slices.Sort(principal) + } + + slices.Sort(stmt.Resource) + slices.Sort(stmt.Action) + } + + return reflect.DeepEqual(b, publicBucketAccessPolicy(bucket).Content) +} + +func (b BucketAccessPolicyContent) isPrivate() bool { + return len(b.Statement) == 0 +} + +func parseCustomPolicy(policy string) (BucketAccessPolicyContent, error) { + var accessPolicyContent BucketAccessPolicyContent + + decoder := json.NewDecoder(strings.NewReader(policy)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&accessPolicyContent); err != nil { + return accessPolicyContent, errors.Wrap(err, ErrInvalidBucketAccessPolicy) + } + return accessPolicyContent, nil +} + +func findAccessBucketPolicyTypeFromContent(bucket string, policyContent BucketAccessPolicyContent) BucketAccessPolicyType { + if policyContent.isPrivate() { + return PrivateBucketAccessPolicy + } + + if policyContent.isPublic(bucket) { + return PublicBucketAccessPolicy + } + + return CustomBucketAccessPolicy +} + +func customBucketAccessPolicy(policy string) (*BucketAccessPolicy, error) { + bucketAccessPolicy := BucketAccessPolicy{ + Type: CustomBucketAccessPolicy, + } + + decoder := json.NewDecoder(strings.NewReader(policy)) + decoder.DisallowUnknownFields() + bucketAcessPolicyContent, err := parseCustomPolicy(policy) + if err != nil { + return nil, err + } + + bucketAccessPolicy.Content = bucketAcessPolicyContent + return &bucketAccessPolicy, nil +} + +func publicBucketAccessPolicy(bucket string) *BucketAccessPolicy { + return &BucketAccessPolicy{ + Type: PublicBucketAccessPolicy, + Content: BucketAccessPolicyContent{ + IAM: IAM{ + Version: "2012-10-17", + Statement: []IAMStatement{ + { + Effect: "Allow", + Principal: map[string][]string{ + "AWS": []string{"*"}, + }, + Action: []string{ + "s3:GetBucketLocation", + "s3:ListBucket", + "s3:ListBucketMultipartUploads", + }, + Resource: []string{ + fmt.Sprintf("arn:aws:s3:::%s", bucket), + }, + }, + { + Effect: "Allow", + Principal: map[string][]string{ + "AWS": []string{"*"}, + }, + Action: []string{ + "s3:AbortMultipartUpload", + "s3:DeleteObject", + "s3:GetObject", + "s3:ListMultipartUploadParts", + "s3:PutObject", + }, + Resource: []string{ + fmt.Sprintf("arn:aws:s3:::%s/*", bucket), + }, + }, + }, + }, + }, + } +} + +func privateBucketAccessPolicy() *BucketAccessPolicy { + return &BucketAccessPolicy{ + Type: PrivateBucketAccessPolicy, + Content: BucketAccessPolicyContent{ + IAM: IAM{ + Version: "2012-10-17", + Statement: []IAMStatement{}, + }, + }, + } +} diff --git a/pkg/s3/model/iam.go b/pkg/s3/model/iam.go new file mode 100644 index 0000000..a0e1e53 --- /dev/null +++ b/pkg/s3/model/iam.go @@ -0,0 +1,13 @@ +package model + +type IAM struct { + Version string `json:"Version"` + Statement []IAMStatement `json:"Statement"` +} + +type IAMStatement struct { + Effect string `json:"Effect,omitempty"` + Principal map[string][]string `json:"Principal,omitempty"` + Action []string `json:"Action,omitempty"` + Resource []string `json:"Resource,omitempty"` +} diff --git a/test/mocks/S3FactoryMock.go b/test/mocks/S3FactoryMock.go index 60adc2a..e9b53cf 100644 --- a/test/mocks/S3FactoryMock.go +++ b/test/mocks/S3FactoryMock.go @@ -19,7 +19,7 @@ package mocks import ( "github.com/stretchr/testify/mock" - s3client "github.com/InseeFrLab/s3-operator/internal/s3/client" + s3client "github.com/InseeFrLab/s3-operator/pkg/s3/client" ) // Mocked Factory diff --git a/test/mocks/mockedS3Client.go b/test/mocks/mockedS3Client.go index 8418004..44b3640 100644 --- a/test/mocks/mockedS3Client.go +++ b/test/mocks/mockedS3Client.go @@ -17,7 +17,8 @@ limitations under the License. package mocks import ( - s3client "github.com/InseeFrLab/s3-operator/internal/s3/client" + s3client "github.com/InseeFrLab/s3-operator/pkg/s3/client" + s3model "github.com/InseeFrLab/s3-operator/pkg/s3/model" "github.com/minio/madmin-go/v3" "github.com/stretchr/testify/mock" ctrl "sigs.k8s.io/controller-runtime" @@ -70,6 +71,20 @@ func (mockedS3Provider *MockedS3Client) DeletePath(bucketname string, path strin return args.Error(0) } +func (mockedS3Provider *MockedS3Client) GetBucketAccessPolicy(bucketname string) (*s3model.BucketAccessPolicy, error) { + s3Logger := ctrl.Log.WithValues("logger", "mockedS3Client") + s3Logger.Info("getting access policy on a bucket", "bucket", bucketname) + args := mockedS3Provider.Called(bucketname) + return args.Get(0).(*s3model.BucketAccessPolicy), args.Error(1) +} + +func (mockedS3Provider *MockedS3Client) SetBucketAccessPolicy(bucketname string, accessPolicyType s3model.BucketAccessPolicyType, accessPolicy string) error { + s3Logger := ctrl.Log.WithValues("logger", "mockedS3Client") + s3Logger.Info("setting an access policy on a bucket", "bucket", bucketname, "policyType", accessPolicyType) + args := mockedS3Provider.Called(bucketname, accessPolicyType, accessPolicy) + return args.Error(0) +} + func (mockedS3Provider *MockedS3Client) GetQuota(name string) (int64, error) { s3Logger := ctrl.Log.WithValues("logger", "mockedS3Client") s3Logger.Info("getting quota on bucket", "bucket", name) diff --git a/test/utils/testUtils.go b/test/utils/testUtils.go index b899496..600198d 100644 --- a/test/utils/testUtils.go +++ b/test/utils/testUtils.go @@ -20,8 +20,8 @@ import ( "fmt" s3v1alpha1 "github.com/InseeFrLab/s3-operator/api/v1alpha1" - s3client "github.com/InseeFrLab/s3-operator/internal/s3/client" - s3factory "github.com/InseeFrLab/s3-operator/internal/s3/factory" + s3client "github.com/InseeFrLab/s3-operator/pkg/s3/client" + s3factory "github.com/InseeFrLab/s3-operator/pkg/s3/factory" "github.com/InseeFrLab/s3-operator/test/mocks" "github.com/minio/madmin-go/v3" "github.com/stretchr/testify/mock"