├── .gitignore ├── resources ├── kustomizeconfig.yaml ├── cosi-driver-ceph.properties ├── ns.yaml ├── sa.yaml ├── rbac.yaml └── deployment.yaml ├── code-of-conduct.md ├── examples ├── bucketclaim.yaml ├── bucketaccess.yaml ├── bucketclass.yaml ├── bucketaccessclass.yaml └── awscliapppod.yaml ├── .github └── workflows │ ├── test-golang.yaml │ └── tag-release.yaml ├── cmd └── ceph-cosi-driver │ ├── main.go │ └── cmd.go ├── pkg ├── driver │ ├── driver.go │ ├── identityserver.go │ ├── identityserver_test.go │ ├── mockclient.go │ ├── provisioner.go │ └── provisioner_test.go └── util │ └── s3client │ ├── s3-handlers.go │ └── policy.go ├── Dockerfile ├── kustomization.yaml ├── go.mod ├── Makefile ├── README.md ├── LICENSE └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | *.tmp 2 | .DS_Store 3 | .build 4 | *.swp 5 | bin 6 | .idea 7 | -------------------------------------------------------------------------------- /resources/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | varReference: 2 | - path: spec/template/spec/containers/image 3 | kind: Deployment 4 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Community Code of Conduct 2 | 3 | Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) 4 | -------------------------------------------------------------------------------- /examples/bucketclaim.yaml: -------------------------------------------------------------------------------- 1 | kind: BucketClaim 2 | apiVersion: objectstorage.k8s.io/v1alpha1 3 | metadata: 4 | name: sample-bucket 5 | spec: 6 | bucketClassName: sample-bcc 7 | protocols: 8 | - s3 9 | -------------------------------------------------------------------------------- /resources/cosi-driver-ceph.properties: -------------------------------------------------------------------------------- 1 | OBJECTSTORAGE_PROVISIONER_IMAGE_ORG=quay.io/containerobjectstorage 2 | OBJECTSTORAGE_PROVISIONER_IMAGE_VERSION=canary 3 | CEPH_DRIVER_IMAGE_ORG=ceph 4 | CEPH_DRIVER_IMAGE_VERSION=latest 5 | -------------------------------------------------------------------------------- /examples/bucketaccess.yaml: -------------------------------------------------------------------------------- 1 | kind: BucketAccess 2 | apiVersion: objectstorage.k8s.io/v1alpha1 3 | metadata: 4 | name: sample-access 5 | spec: 6 | bucketClaimName: sample-bucket 7 | bucketAccessClassName: sample-bac 8 | credentialsSecretName: sample-access-secret 9 | protocol: s3 10 | -------------------------------------------------------------------------------- /resources/ns.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: ceph-cosi-driver 6 | labels: 7 | app.kubernetes.io/part-of: container-object-storage-interface 8 | app.kubernetes.io/component: driver-ceph 9 | app.kubernetes.io/version: main 10 | app.kubernetes.io/name: cosi-driver-ceph 11 | -------------------------------------------------------------------------------- /examples/bucketclass.yaml: -------------------------------------------------------------------------------- 1 | kind: BucketClass 2 | apiVersion: objectstorage.k8s.io/v1alpha1 3 | metadata: 4 | name: sample-bcc 5 | driverName: cosi.ceph.objectstorage.k8s.io 6 | deletionPolicy: Delete 7 | parameters: 8 | objectStoreUserSecretName: rook-ceph-object-user-my-store-cosi 9 | objectStoreUserSecretNamespace: rook-ceph 10 | -------------------------------------------------------------------------------- /examples/bucketaccessclass.yaml: -------------------------------------------------------------------------------- 1 | kind: BucketAccessClass 2 | apiVersion: objectstorage.k8s.io/v1alpha1 3 | metadata: 4 | name: sample-bac 5 | driverName: cosi.ceph.objectstorage.k8s.io 6 | authenticationType: KEY 7 | parameters: 8 | objectStoreUserSecretName: rook-ceph-object-user-my-store-cosi 9 | objectStoreUserSecretNamespace: rook-ceph 10 | -------------------------------------------------------------------------------- /resources/sa.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: objectstorage-provisioner-sa 6 | namespace: default # must set to default. see https://github.com/kubernetes-sigs/kustomize/issues/1377#issuecomment-694731163 7 | labels: 8 | app.kubernetes.io/part-of: container-object-storage-interface 9 | app.kubernetes.io/component: driver-ceph 10 | app.kubernetes.io/version: main 11 | app.kubernetes.io/name: cosi-driver-ceph 12 | -------------------------------------------------------------------------------- /examples/awscliapppod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: awscli 5 | spec: 6 | containers: 7 | - name: awscli 8 | # TODO: Replace the image with an official one once Amazon publishes theirs 9 | image: mikesir87/aws-cli:1.16.220 10 | stdin: true 11 | tty: true 12 | volumeMounts: 13 | - name: cosi-secrets 14 | mountPath: /data/cosi 15 | readOnly: true 16 | volumes: 17 | - name: cosi-secrets 18 | secret: 19 | secretName: sample-access-secret 20 | -------------------------------------------------------------------------------- /.github/workflows/test-golang.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run Golang tests 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | pull_request: 7 | branches: 8 | - "*" 9 | 10 | jobs: 11 | make_test: 12 | name: make_test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out the repo 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Golang 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: "1.22" 22 | 23 | - name: Install Protoc 24 | uses: arduino/setup-protoc@v1 25 | with: 26 | version: '3.19.6' 27 | 28 | - name: Run "make vet" 29 | run: make vet 30 | 31 | - name: Run "make fmt" 32 | run: make fmt 33 | 34 | - name: Run "make test" 35 | run: make test 36 | 37 | - name: Show the uncommitted "git diff" 38 | if: ${{ failure() }} 39 | run: git diff ; false 40 | -------------------------------------------------------------------------------- /cmd/ceph-cosi-driver/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Ceph-COSI Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "os/signal" 23 | "syscall" 24 | "time" 25 | 26 | "k8s.io/klog/v2" 27 | ) 28 | 29 | func main() { 30 | ctx, cancel := context.WithCancel(context.Background()) 31 | defer cancel() 32 | 33 | sigs := make(chan os.Signal, 1) 34 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 35 | 36 | go func() { 37 | sig := <-sigs 38 | klog.InfoS("Signal received", "type", sig) 39 | cancel() 40 | 41 | <-time.After(30 * time.Second) 42 | os.Exit(1) 43 | }() 44 | 45 | if err := run(ctx); err != nil { 46 | klog.ErrorS(err, "Exiting on error") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/driver/driver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Ceph-COSI Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "context" 21 | 22 | "k8s.io/klog/v2" 23 | cosispec "sigs.k8s.io/container-object-storage-interface/proto" 24 | ) 25 | 26 | func NewDriver(ctx context.Context, driverName string) (cosispec.IdentityServer, cosispec.ProvisionerServer, error) { 27 | provisionerServer, err := NewProvisionerServer(driverName) 28 | if err != nil { 29 | klog.Fatal(err, "failed to create provisioner server") 30 | return nil, nil, err 31 | } 32 | identityServer, err := NewIdentityServer(driverName) 33 | if err != nil { 34 | klog.Fatal(err, "failed to create provisioner server") 35 | return nil, nil, err 36 | } 37 | return identityServer, provisionerServer, nil 38 | } 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # BUILDER 3 | # 4 | 5 | FROM golang:1.22 AS builder 6 | ARG TARGETOS 7 | ARG TARGETARCH 8 | 9 | WORKDIR /workspace 10 | # Copy the Go Modules manifests 11 | COPY go.mod go.mod 12 | COPY go.sum go.sum 13 | # cache deps before building and copying source so that we don't need to re-download as much 14 | # and so that source changes don't invalidate our downloaded layer 15 | RUN go mod download 16 | 17 | # Copy the go source 18 | COPY cmd/ cmd/ 19 | COPY pkg/ pkg/ 20 | 21 | # Build 22 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 23 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 24 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 25 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 26 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o ceph-cosi-driver cmd/ceph-cosi-driver/*.go 27 | 28 | # 29 | # FINAL IMAGE 30 | # 31 | 32 | # Use distroless as minimal base image to package the binary 33 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 34 | FROM gcr.io/distroless/static:nonroot 35 | 36 | LABEL maintainers="Ceph COSI Authors" 37 | LABEL description="Ceph COSI driver" 38 | 39 | WORKDIR / 40 | COPY --from=builder /workspace/ceph-cosi-driver . 41 | USER 65532:65532 42 | 43 | ENTRYPOINT ["/ceph-cosi-driver"] -------------------------------------------------------------------------------- /resources/rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: ClusterRole 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: objectstorage-provisioner-role 6 | labels: 7 | app.kubernetes.io/part-of: container-object-storage-interface 8 | app.kubernetes.io/component: driver-ceph 9 | app.kubernetes.io/version: main 10 | app.kubernetes.io/name: cosi-driver-ceph 11 | rules: 12 | - apiGroups: ["objectstorage.k8s.io"] 13 | resources: ["buckets", "bucketaccesses", "bucketclaims", "bucketaccessclasses", "buckets/status", "bucketaccesses/status", "bucketclaims/status", "bucketaccessclasses/status"] 14 | verbs: ["get", "list", "watch", "update", "create", "delete"] 15 | - apiGroups: ["coordination.k8s.io"] 16 | resources: ["leases"] 17 | verbs: ["get", "watch", "list", "delete", "update", "create"] 18 | - apiGroups: [""] 19 | resources: ["secrets", "events"] 20 | verbs: ["get", "delete", "update", "create"] 21 | --- 22 | kind: ClusterRoleBinding 23 | apiVersion: rbac.authorization.k8s.io/v1 24 | metadata: 25 | name: objectstorage-provisioner-role-binding 26 | labels: 27 | app.kubernetes.io/part-of: container-object-storage-interface 28 | app.kubernetes.io/component: driver-ceph 29 | app.kubernetes.io/version: main 30 | app.kubernetes.io/name: cosi-driver-ceph 31 | subjects: 32 | - kind: ServiceAccount 33 | name: objectstorage-provisioner-sa 34 | namespace: default # must set to default. see https://github.com/kubernetes-sigs/kustomize/issues/1377#issuecomment-694731163 35 | roleRef: 36 | kind: ClusterRole 37 | name: objectstorage-provisioner-role 38 | apiGroup: rbac.authorization.k8s.io 39 | -------------------------------------------------------------------------------- /pkg/driver/identityserver.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Ceph-COSI Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "google.golang.org/grpc/codes" 24 | "google.golang.org/grpc/status" 25 | "k8s.io/klog/v2" 26 | 27 | cosispec "sigs.k8s.io/container-object-storage-interface/proto" 28 | ) 29 | 30 | type identityServer struct { 31 | cosispec.UnimplementedIdentityServer 32 | 33 | provisioner string 34 | } 35 | 36 | var _ cosispec.IdentityServer = &identityServer{} 37 | 38 | func NewIdentityServer(provisionerName string) (cosispec.IdentityServer, error) { 39 | return &identityServer{ 40 | provisioner: provisionerName, 41 | }, nil 42 | } 43 | func (id *identityServer) DriverGetInfo(ctx context.Context, 44 | req *cosispec.DriverGetInfoRequest) (*cosispec.DriverGetInfoResponse, error) { 45 | 46 | if id.provisioner == "" { 47 | klog.ErrorS(fmt.Errorf("provisioner name cannot be empty"), "invalid argument") 48 | return nil, status.Error(codes.InvalidArgument, "Provisioner name is empty") 49 | } 50 | 51 | return &cosispec.DriverGetInfoResponse{ 52 | Name: id.provisioner, 53 | }, nil 54 | } 55 | -------------------------------------------------------------------------------- /kustomization.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | namespace: ceph-cosi-driver 5 | 6 | commonAnnotations: 7 | cosi.storage.k8s.io/authors: "Kubernetes Authors" 8 | cosi.storage.k8s.io/license: "Apache V2" 9 | cosi.storage.k8s.io/support: "https://github.com/kubernetes-sigs/container-object-storage-api" 10 | 11 | commonLabels: 12 | app.kubernetes.io/part-of: container-object-storage-interface 13 | app.kubernetes.io/component: driver-ceph 14 | app.kubernetes.io/version: main 15 | app.kubernetes.io/name: cosi-driver-ceph 16 | 17 | configMapGenerator: 18 | - name: cosi-driver-ceph-config 19 | env: resources/cosi-driver-ceph.properties 20 | generatorOptions: 21 | disableNameSuffixHash: true 22 | labels: 23 | generated-by: "kustomize" 24 | 25 | resources: 26 | - resources/ns.yaml 27 | - resources/sa.yaml 28 | - resources/rbac.yaml 29 | - resources/deployment.yaml 30 | 31 | configurations: 32 | - resources/kustomizeconfig.yaml 33 | 34 | vars: 35 | - name: IMAGE_ORG 36 | objref: 37 | name: cosi-driver-ceph-config 38 | kind: ConfigMap 39 | apiVersion: v1 40 | fieldref: 41 | fieldpath: data.OBJECTSTORAGE_PROVISIONER_IMAGE_ORG 42 | - name: IMAGE_VERSION 43 | objref: 44 | name: cosi-driver-ceph-config 45 | kind: ConfigMap 46 | apiVersion: v1 47 | fieldref: 48 | fieldpath: data.OBJECTSTORAGE_PROVISIONER_IMAGE_VERSION 49 | - name: CEPH_IMAGE_ORG 50 | objref: 51 | name: cosi-driver-ceph-config 52 | kind: ConfigMap 53 | apiVersion: v1 54 | fieldref: 55 | fieldpath: data.CEPH_DRIVER_IMAGE_ORG 56 | - name: CEPH_IMAGE_VERSION 57 | objref: 58 | name: cosi-driver-ceph-config 59 | kind: ConfigMap 60 | apiVersion: v1 61 | fieldref: 62 | fieldpath: data.CEPH_DRIVER_IMAGE_VERSION 63 | -------------------------------------------------------------------------------- /cmd/ceph-cosi-driver/cmd.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Ceph-COSI Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "flag" 23 | 24 | "github.com/ceph/cosi-driver-ceph/pkg/driver" 25 | 26 | "k8s.io/klog/v2" 27 | 28 | "sigs.k8s.io/container-object-storage-interface/sidecar/pkg/provisioner" 29 | ) 30 | 31 | const provisionerName = "ceph.objectstorage.k8s.io" 32 | 33 | var ( 34 | driverAddress = flag.String("driver-address", "unix:///var/lib/cosi/cosi.sock", "driver address for socket") 35 | driverPrefix = flag.String("driver-prefix", "", "prefix for cosi driver, e.g. .ceph.objectstorage.k8s.io") 36 | ) 37 | 38 | func init() { 39 | klog.InitFlags(nil) 40 | if err := flag.Set("logtostderr", "true"); err != nil { 41 | klog.Exitf("failed to set logtostderr flag: %v", err) 42 | } 43 | flag.Parse() 44 | } 45 | 46 | func run(ctx context.Context) error { 47 | if *driverPrefix == "" { 48 | return errors.New("driver prefix is missing for ceph cosi driver deployment") 49 | } 50 | driverName := *driverPrefix + "." + provisionerName 51 | identityServer, bucketProvisioner, err := driver.NewDriver(ctx, driverName) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | server, err := provisioner.NewDefaultCOSIProvisionerServer(*driverAddress, 57 | identityServer, 58 | bucketProvisioner) 59 | if err != nil { 60 | return err 61 | } 62 | return server.Run(ctx) 63 | } 64 | -------------------------------------------------------------------------------- /resources/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: objectstorage-provisioner 5 | labels: 6 | app.kubernetes.io/part-of: container-object-storage-interface 7 | app.kubernetes.io/component: driver-ceph 8 | app.kubernetes.io/version: main 9 | app.kubernetes.io/name: cosi-driver-ceph 10 | spec: 11 | replicas: 1 12 | minReadySeconds: 30 13 | progressDeadlineSeconds: 600 14 | revisionHistoryLimit: 3 15 | strategy: 16 | type: RollingUpdate 17 | rollingUpdate: 18 | maxSurge: 1 19 | maxUnavailable: 0 20 | selector: 21 | matchLabels: 22 | app.kubernetes.io/part-of: container-object-storage-interface 23 | app.kubernetes.io/component: driver-ceph 24 | app.kubernetes.io/version: main 25 | app.kubernetes.io/name: cosi-driver-ceph 26 | template: 27 | metadata: 28 | labels: 29 | app.kubernetes.io/part-of: container-object-storage-interface 30 | app.kubernetes.io/component: driver-ceph 31 | app.kubernetes.io/version: main 32 | app.kubernetes.io/name: cosi-driver-ceph 33 | spec: 34 | securityContext: 35 | fsGroup: 65532 36 | runAsGroup: 65532 37 | runAsUser: 65532 38 | serviceAccountName: objectstorage-provisioner-sa 39 | volumes: 40 | - name: socket 41 | emptyDir: {} 42 | containers: 43 | - name: ceph-cosi-driver 44 | image: $(CEPH_IMAGE_ORG)/ceph-cosi-driver:$(CEPH_IMAGE_VERSION) 45 | imagePullPolicy: IfNotPresent 46 | args: 47 | - "--driver-prefix=cosi" 48 | volumeMounts: 49 | - mountPath: /var/lib/cosi 50 | name: socket 51 | env: 52 | - name: POD_NAMESPACE 53 | valueFrom: 54 | fieldRef: 55 | fieldPath: metadata.namespace 56 | - name: objectstorage-provisioner-sidecar 57 | image: gcr.io/k8s-staging-sig-storage/objectstorage-sidecar:latest 58 | imagePullPolicy: IfNotPresent 59 | args: 60 | - "--v=5" 61 | volumeMounts: 62 | - mountPath: /var/lib/cosi 63 | name: socket 64 | -------------------------------------------------------------------------------- /.github/workflows/tag-release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build and release versioned container images 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | create 7 | 8 | jobs: 9 | tag_image: 10 | name: Build and release the bundle container-image 11 | if: > 12 | github.repository == 'ceph/ceph-cosi' 13 | && 14 | github.ref_type == 'tag' 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out the repo 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Golang 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version: "1.22" 24 | 25 | - name: Generate the container image 26 | run: make container 27 | 28 | - name: Login to quay.io 29 | uses: docker/login-action@v3 30 | with: 31 | registry: quay.io 32 | username: ${{ secrets.QUAY_USERNAME }} 33 | password: ${{ secrets.QUAY_PASSWORD }} 34 | 35 | - name: Set up Docker Buildx 36 | uses: docker/setup-buildx-action@v3 37 | 38 | - name: Set up QEMU for multi-arch builds 39 | uses: docker/setup-qemu-action@v3 40 | with: 41 | platforms: 'arm64' 42 | 43 | - name: Build bundle container image 44 | uses: docker/build-push-action@v5 45 | with: 46 | context: . 47 | file: Dockerfile 48 | push: true 49 | tags: quay.io/ceph/cosi:${{ github.ref_name }} 50 | platforms: linux/amd64,linux/arm64 51 | 52 | publish_release: 53 | name: Publish a release based on the tag 54 | if: > 55 | github.repository == 'ceph/ceph-cosi' 56 | && 57 | github.ref_type == 'tag' 58 | runs-on: ubuntu-latest 59 | permissions: 60 | contents: write 61 | steps: 62 | - name: Check out the repo 63 | uses: actions/checkout@v4 64 | 65 | - name: Setup Golang 66 | uses: actions/setup-go@v5 67 | with: 68 | go-version: "1.22" 69 | 70 | - name: Publish the release and attach YAML files 71 | uses: ncipollo/release-action@v1 72 | with: 73 | tag: ${{ github.ref_name }} 74 | artifacts: "examples/*.yaml" 75 | generateReleaseNotes: true 76 | token: ${{ secrets.GITHUB_TOKEN }} 77 | -------------------------------------------------------------------------------- /pkg/driver/identityserver_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Ceph-COSI Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package driver 18 | 19 | import ( 20 | "context" 21 | "reflect" 22 | "testing" 23 | 24 | cosi "sigs.k8s.io/container-object-storage-interface/proto" 25 | ) 26 | 27 | func TestIdentityServer_DriverGetInfo(t *testing.T) { 28 | type fields struct { 29 | provisioner string 30 | } 31 | type args struct { 32 | ctx context.Context 33 | req *cosi.DriverGetInfoRequest 34 | } 35 | tests := []struct { 36 | name string 37 | fields fields 38 | args args 39 | want *cosi.DriverGetInfoResponse 40 | wantErr bool 41 | }{ 42 | { 43 | name: "Test DriverGetInfo", 44 | fields: fields{ 45 | provisioner: "ceph-cosi-driver", 46 | }, 47 | args: args{ 48 | ctx: context.Background(), 49 | req: &cosi.DriverGetInfoRequest{}, 50 | }, 51 | want: &cosi.DriverGetInfoResponse{ 52 | Name: "ceph-cosi-driver", 53 | }, 54 | wantErr: false, 55 | }, 56 | { 57 | name: "Test DriverGetInfo with empty provisioner name", 58 | fields: fields{ 59 | provisioner: "", 60 | }, 61 | args: args{ 62 | ctx: context.Background(), 63 | req: &cosi.DriverGetInfoRequest{}, 64 | }, 65 | want: nil, 66 | wantErr: true, 67 | }, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | id := &identityServer{ 72 | provisioner: tt.fields.provisioner, 73 | } 74 | got, err := id.DriverGetInfo(tt.args.ctx, tt.args.req) 75 | if (err != nil) != tt.wantErr { 76 | t.Errorf("IdentityServer.DriverGetInfo() error = %v, wantErr %v", err, tt.wantErr) 77 | return 78 | } 79 | if !reflect.DeepEqual(got, tt.want) { 80 | t.Errorf("IdentityServer.DriverGetInfo() = %v, want %v", got, tt.want) 81 | } 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ceph/cosi-driver-ceph 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.51.12 7 | github.com/ceph/go-ceph v0.27.0 8 | google.golang.org/grpc v1.75.1 9 | k8s.io/apimachinery v0.32.3 10 | k8s.io/client-go v0.32.3 11 | k8s.io/klog/v2 v2.130.1 12 | sigs.k8s.io/container-object-storage-interface v0.0.0-20250915185608-01dcd1a8c124 13 | sigs.k8s.io/container-object-storage-interface/client v0.0.0-20250915175017-b1ac3c818b6e 14 | sigs.k8s.io/container-object-storage-interface/proto v0.0.0-20250728140943-f18af7ae56c9 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 19 | github.com/emicklei/go-restful/v3 v3.12.0 // indirect 20 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 21 | github.com/go-logr/logr v1.4.3 // indirect 22 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 23 | github.com/go-openapi/jsonreference v0.21.0 // indirect 24 | github.com/go-openapi/swag v0.23.0 // indirect 25 | github.com/gogo/protobuf v1.3.2 // indirect 26 | github.com/golang/protobuf v1.5.4 // indirect 27 | github.com/google/gnostic-models v0.6.9 // indirect 28 | github.com/google/go-cmp v0.7.0 // indirect 29 | github.com/google/gofuzz v1.2.0 // indirect 30 | github.com/google/uuid v1.6.0 // indirect 31 | github.com/jmespath/go-jmespath v0.4.0 // indirect 32 | github.com/josharian/intern v1.0.0 // indirect 33 | github.com/json-iterator/go v1.1.12 // indirect 34 | github.com/mailru/easyjson v0.7.7 // indirect 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 36 | github.com/modern-go/reflect2 v1.0.2 // indirect 37 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 38 | github.com/pkg/errors v0.9.1 // indirect 39 | github.com/x448/float16 v0.8.4 // indirect 40 | golang.org/x/net v0.41.0 // indirect 41 | golang.org/x/oauth2 v0.30.0 // indirect 42 | golang.org/x/sys v0.33.0 // indirect 43 | golang.org/x/term v0.32.0 // indirect 44 | golang.org/x/text v0.28.0 // indirect 45 | golang.org/x/time v0.9.0 // indirect 46 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect 47 | google.golang.org/protobuf v1.36.6 // indirect 48 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 49 | gopkg.in/inf.v0 v0.9.1 // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | k8s.io/api v0.32.3 // indirect 52 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 53 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 54 | sigs.k8s.io/controller-runtime v0.18.4 // indirect 55 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 56 | sigs.k8s.io/randfill v1.0.0 // indirect 57 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 58 | sigs.k8s.io/yaml v1.4.0 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /pkg/driver/mockclient.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/aws/aws-sdk-go/aws/awserr" 7 | "github.com/aws/aws-sdk-go/service/s3" 8 | "github.com/aws/aws-sdk-go/service/s3/s3iface" 9 | ) 10 | 11 | // MockClient is the mock of the HTTP Client 12 | // It can be used to mock HTTP request/response from the rgw admin ops API 13 | type MockClient struct { 14 | // MockDo is a type that mock the Do method from the HTTP package 15 | MockDo MockDoType 16 | } 17 | 18 | // MockDoType is a custom type that allows setting the function that our Mock Do func will run instead 19 | type MockDoType func(req *http.Request) (*http.Response, error) 20 | 21 | // Do is the mock client's `Do` func 22 | func (m *MockClient) Do(req *http.Request) (*http.Response, error) { return m.MockDo(req) } 23 | 24 | type mockS3Client struct { 25 | s3iface.S3API 26 | } 27 | 28 | func (m mockS3Client) CreateBucket(input *s3.CreateBucketInput) (*s3.CreateBucketOutput, error) { 29 | switch *input.Bucket { 30 | case "test-bucket": 31 | return &s3.CreateBucketOutput{}, nil 32 | case "test-bucket-owned-by-you": 33 | return nil, awserr.New("BucketAlreadyOwnedByYou", "BucketAlreadyOwnedByYou", nil) 34 | case "test-bucket-fail-internal": 35 | return nil, awserr.New("InternalError", "InternalError", nil) 36 | case "test-bucket-already-exists": 37 | return nil, awserr.New("BucketAlreadyExists", "BucketAlreadyExists", nil) 38 | } 39 | return nil, awserr.New("InvalidBucketName", "InvalidBucketName", nil) 40 | } 41 | 42 | func (m mockS3Client) DeleteBucket(input *s3.DeleteBucketInput) (*s3.DeleteBucketOutput, error) { 43 | switch *input.Bucket { 44 | case "test-bucket": 45 | return &s3.DeleteBucketOutput{}, nil 46 | case "test-bucket-not-empty": 47 | return nil, awserr.New("BucketNotEmpty", "BucketNotEmpty", nil) 48 | case "test-bucket-fail-internal": 49 | return nil, awserr.New("InternalError", "InternalError", nil) 50 | } 51 | return nil, awserr.New("NoSuchBucket", "NoSuchBucket", nil) 52 | } 53 | 54 | func (m mockS3Client) PutBucketPolicy(input *s3.PutBucketPolicyInput) (*s3.PutBucketPolicyOutput, error) { 55 | switch *input.Bucket { 56 | case "test-bucket": 57 | return &s3.PutBucketPolicyOutput{}, nil 58 | case "test-bucket-fail-internal": 59 | return nil, awserr.New("InternalError", "InternalError", nil) 60 | } 61 | return nil, awserr.New("NoSuchBucket", "NoSuchBucket", nil) 62 | } 63 | 64 | func (m mockS3Client) GetBucketPolicy(input *s3.GetBucketPolicyInput) (*s3.GetBucketPolicyOutput, error) { 65 | switch *input.Bucket { 66 | case "test-bucket": 67 | policy := `{"Version":"2012-10-17","Statement":[{"Sid":"AddPerm","Effect":"Allow","Principal":"*","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::test-bucket/*"]}]}` 68 | return &s3.GetBucketPolicyOutput{Policy: &policy}, nil 69 | case "test-bucket-fail-internal": 70 | return nil, awserr.New("InternalError", "InternalError", nil) 71 | } 72 | return nil, awserr.New("NoSuchBucket", "NoSuchBucket", nil) 73 | } 74 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 The Kubernetes Authors. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | CMDS=ceph-cosi-driver 16 | 17 | REGISTRY_NAME=quay.io/ceph/cosi 18 | IMAGE_TAGS=latest 19 | 20 | all: build 21 | 22 | .PHONY: build-% build container-% container clean 23 | 24 | # A space-separated list of all commands in the repository, must be 25 | # set in main Makefile of a repository. 26 | # CMDS= 27 | 28 | # Revision that gets built into each binary via the main.version 29 | # string. Uses the `git describe` output based on the most recent 30 | # version tag with a short revision suffix or, if nothing has been 31 | # tagged yet, just the revision. 32 | # 33 | # Beware that tags may also be missing in shallow clones as done by 34 | # some CI systems (like TravisCI, which pulls only 50 commits). 35 | REV=$(shell git describe --long --tags --match='v*' --dirty 2>/dev/null || git rev-list -n1 HEAD) 36 | 37 | # A space-separated list of image tags under which the current build is to be pushed. 38 | # Determined dynamically. 39 | IMAGE_TAGS= 40 | 41 | # A "canary" image gets built if the current commit is the head of the remote "master" branch. 42 | # That branch does not exist when building some other branch in TravisCI. 43 | IMAGE_TAGS+=$(shell if [ "$$(git rev-list -n1 HEAD)" = "$$(git rev-list -n1 origin/master 2>/dev/null)" ]; then echo "canary"; fi) 44 | 45 | # A "X.Y.Z-canary" image gets built if the current commit is the head of a "origin/release-X.Y.Z" branch. 46 | # The actual suffix does not matter, only the "release-" prefix is checked. 47 | IMAGE_TAGS+=$(shell git branch -r --points-at=HEAD | grep 'origin/release-' | grep -v -e ' -> ' | sed -e 's;.*/release-\(.*\);\1-canary;') 48 | 49 | # A release image "vX.Y.Z" gets built if there is a tag of that format for the current commit. 50 | # --abbrev=0 suppresses long format, only showing the closest tag. 51 | IMAGE_TAGS+=$(shell tagged="$$(git describe --tags --match='v*' --abbrev=0)"; if [ "$$tagged" ] && [ "$$(git rev-list -n1 HEAD)" = "$$(git rev-list -n1 $$tagged)" ]; then echo $$tagged; fi) 52 | 53 | # Images are named after the command contained in them. 54 | IMAGE_NAME=$(REGISTRY_NAME)/$* 55 | 56 | ARCH := $(if $(GOARCH),$(GOARCH),$(shell go env GOARCH)) 57 | 58 | # detect container tools, prefer Podman over Docker 59 | CONTAINER_CMD ?= $(shell podman version >/dev/null 2>&1 && echo podman) 60 | ifeq ($(CONTAINER_CMD),) 61 | CONTAINER_CMD = $(shell docker version >/dev/null 2>&1 && echo docker) 62 | endif 63 | 64 | # Specific packages can be excluded from each of the tests below by setting the *_FILTER_CMD variables 65 | # to something like "| grep -v 'github.com/kubernetes-csi/project/pkg/foobar'". See usage below. 66 | 67 | build-%: 68 | mkdir -p bin 69 | CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-X main.version=$(REV) -extldflags "-static"' -o ./bin/$* ./cmd/$* 70 | if [ "$$ARCH" = "amd64" ]; then \ 71 | CGO_ENABLED=0 GOOS=windows go build -a -ldflags '-X main.version=$(REV) -extldflags "-static"' -o ./bin/$*.exe ./cmd/$* ; \ 72 | CGO_ENABLED=0 GOOS=linux GOARCH=ppc64le go build -a -ldflags '-X main.version=$(REV) -extldflags "-static"' -o ./bin/$*-ppc64le ./cmd/$* ; \ 73 | fi 74 | 75 | build: $(CMDS:%=build-%) 76 | 77 | container: 78 | $(CONTAINER_CMD) build --tag ceph-cosi-driver:latest --label revision=$(REV) . 79 | 80 | clean: 81 | -rm -rf bin 82 | 83 | .PHONY: fmt 84 | fmt: ## Run go fmt against code. 85 | go fmt ./... 86 | 87 | .PHONY: vet 88 | vet: ## Run go vet against code. 89 | go vet ./... 90 | 91 | .PHONY: test 92 | test: ## Run unit tests against code. 93 | go test ./... -coverprofile=coverage.txt -covermode=atomic 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cosi-driver-ceph 2 | 3 | Sample Driver that provides reference implementation for Container Object Storage Interface (COSI) API for [Ceph Object Store aka RADOS Gateway (RGW)](https://docs.ceph.com/en/latest/man/8/radosgw/) 4 | 5 | ## Installing CRDs, COSI controller, Node adapter 6 | 7 | ```console 8 | kubectl create -k github.com/kubernetes-sigs/container-object-storage-interface-api 9 | 10 | kubectl create -k github.com/kubernetes-sigs/container-object-storage-interface-controller 11 | ``` 12 | 13 | Following pods will running in the default namespace : 14 | 15 | ```console 16 | NAME READY STATUS RESTARTS AGE 17 | objectstorage-controller-6fc5f89444-4ws72 1/1 Running 0 2d6h 18 | ``` 19 | 20 | ## Building, Installing, Setting Up 21 | 22 | Code can be compiled using: 23 | 24 | ```bash 25 | make build 26 | ``` 27 | 28 | Now build docker image and provide tag as `ceph/ceph-cosi-driver:latest` 29 | 30 | ```console 31 | make container 32 | Sending build context to Docker daemon 41.95MB 33 | Step 1/5 : FROM gcr.io/distroless/static:latest 34 | ---> 1d9948f921db 35 | Step 2/5 : LABEL maintainers="Ceph COSI Authors" 36 | ---> Using cache 37 | ---> 8659e9813ec5 38 | Step 3/5 : LABEL description="Ceph COSI driver" 39 | ---> Using cache 40 | ---> 0c55b21ff64f 41 | Step 4/5 : COPY ./cmd/ceph-cosi-driver/ceph-cosi-driver ceph-cosi-driver 42 | ---> a21275402998 43 | Step 5/5 : ENTRYPOINT ["/ceph-cosi-driver"] 44 | ---> Running in 620bfa992683 45 | Removing intermediate container 620bfa992683 46 | ---> 09575229056e 47 | Successfully built 09575229056e 48 | 49 | docker tag ceph-cosi-driver:latest ceph/ceph-cosi-driver:latest 50 | ``` 51 | 52 | Now start the sidecar and cosi driver with: 53 | 54 | ```console 55 | kubectl apply -k . 56 | kubectl -n ceph-cosi-driver get pods 57 | NAME READY STATUS RESTARTS AGE 58 | objectstorage-provisioner-6c8df56cc6-lqr26 2/2 Running 0 26h 59 | ``` 60 | 61 | ## Create Bucket Requests, Bucket Access Request and consuming it in App 62 | 63 | ```console 64 | kubectl create -f examples/bucketclass.yaml 65 | kubectl create -f examples/bucketclaim.yaml 66 | kubectl create -f examples/bucketaccessclass.yaml 67 | kubectl create -f examples/bucketaccess.yaml 68 | ``` 69 | 70 | Need to provide access details for RGW server via secret and it needs to be referenced in BucketAccessClass and BucketClass. 71 | 72 | ```yaml 73 | parameters: 74 | objectStoreUserSecretName: 75 | objectStoreUserSecretNamespace: 76 | ``` 77 | 78 | In the app, credentials can be consumed as secret volume mount using the secret name specified in the BucketAccess: 79 | 80 | ```yaml 81 | spec: 82 | containers: 83 | volumeMounts: 84 | - name: cosi-secrets 85 | mountPath: /data/cosi 86 | volumes: 87 | - name: cosi-secrets 88 | secret: 89 | secretName: sample-access-secret 90 | ``` 91 | 92 | An example for awscli pods can be found at `examples/awscliapppod.yaml`. Credentials will be in json format in the file. 93 | 94 | ```json 95 | { 96 | apiVersion: "v1alpha1", 97 | kind: "BucketInfo", 98 | metadata: { 99 | name: "ba-$uuid" 100 | }, 101 | spec: { 102 | bucketName: "ba-$uuid", 103 | authenticationType: "KEY", 104 | endpoint: "https://rook-ceph-my-store:443", 105 | accessKeyID: "AKIAIOSFODNN7EXAMPLE", 106 | accessSecretKey: "wJalrXUtnFEMI/K...", 107 | region: "us-east-1", 108 | protocols: [ 109 | "s3" 110 | ] 111 | } 112 | } 113 | ``` 114 | 115 | ## Known limitations 116 | 117 | 1. Handle access policies for Bucket Access Request 118 | 119 | ## Configuration Options 120 | 121 | | Option | Default value | Description | 122 | | ------------------------- | -------------------------------------- | -------------------------------------------------------------------| 123 | | `--driver-address` | `unix:///var/lib/cosi/cosi.sock` | COSI driver address, must be a UNIX socket | 124 | | `--driver-prefix` | _empty_ | prefix added before name, e.g, `.ceph.objectstorage.k8s.io`| 125 | 126 | ## Integration with Rook 127 | 128 | The ceph cosi driver integrates with [Rook](https://rook.io/) from v1.12 onwards to provide object storage for Kubernetes applications. More details can be found [here](https://rook.io/docs/rook/v1.12/Storage-Configuration/Object-Storage-RGW/cosi/). 129 | 130 | ## Community, discussion, contribution, and support 131 | 132 | You can reach the maintainers of this project at: 133 | 134 | - [Slack](https://kubernetes.slack.com/messages/sig-storage) 135 | - [Mailing List](https://groups.google.com/forum/#!forum/kubernetes-sig-storage) 136 | 137 | ## Code of conduct 138 | 139 | Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). 140 | -------------------------------------------------------------------------------- /pkg/util/s3client/s3-handlers.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Ceph-COSI Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package s3client 18 | 19 | import ( 20 | "bytes" 21 | "crypto/tls" 22 | "crypto/x509" 23 | "net/http" 24 | "strings" 25 | "time" 26 | 27 | "github.com/aws/aws-sdk-go/aws" 28 | "github.com/aws/aws-sdk-go/aws/awserr" 29 | "github.com/aws/aws-sdk-go/aws/credentials" 30 | "github.com/aws/aws-sdk-go/aws/session" 31 | "github.com/aws/aws-sdk-go/service/s3" 32 | "github.com/aws/aws-sdk-go/service/s3/s3iface" 33 | "k8s.io/klog/v2" 34 | ) 35 | 36 | const ( 37 | rgwRegion = "us-east-1" 38 | HttpTimeOut = 15 * time.Second 39 | ) 40 | 41 | // S3Agent wraps the s3iface structure to allow for wrapper methods 42 | type S3Agent struct { 43 | Client s3iface.S3API 44 | } 45 | 46 | func NewS3Agent(accessKey, secretKey, endpoint string, tlsCert []byte, debug bool) (*S3Agent, error) { 47 | logLevel := aws.LogOff 48 | if debug { 49 | logLevel = aws.LogDebug 50 | } 51 | client := http.Client{ 52 | Timeout: HttpTimeOut, 53 | } 54 | tlsEnabled := false 55 | insecure := false 56 | if strings.HasPrefix(endpoint, "https") && len(tlsCert) == 0 { 57 | insecure = true 58 | } 59 | if len(tlsCert) > 0 || insecure { 60 | tlsEnabled = true 61 | client.Transport = buildTransportTLS(tlsCert, insecure) 62 | } 63 | session, err := session.NewSession( 64 | aws.NewConfig(). 65 | WithRegion(rgwRegion). 66 | WithCredentials(credentials.NewStaticCredentials(accessKey, secretKey, "")). 67 | WithEndpoint(endpoint). 68 | WithS3ForcePathStyle(true). 69 | WithMaxRetries(5). 70 | WithDisableSSL(!tlsEnabled). 71 | WithHTTPClient(&client). 72 | WithLogLevel(logLevel), 73 | ) 74 | if err != nil { 75 | return nil, err 76 | } 77 | svc := s3.New(session) 78 | return &S3Agent{ 79 | Client: svc, 80 | }, nil 81 | } 82 | 83 | // CreateBucket creates a bucket with the given name 84 | func (s *S3Agent) CreateBucketNoInfoLogging(name string) error { 85 | return s.createBucket(name, false) 86 | } 87 | 88 | // CreateBucket creates a bucket with the given name 89 | func (s *S3Agent) CreateBucket(name string) error { 90 | return s.createBucket(name, true) 91 | } 92 | 93 | func (s *S3Agent) createBucket(name string, infoLogging bool) error { 94 | if infoLogging { 95 | klog.InfoS("creating bucket", "name", name) 96 | } else { 97 | klog.InfoS("creating bucket", "name", name) 98 | } 99 | bucketInput := &s3.CreateBucketInput{ 100 | Bucket: &name, 101 | } 102 | _, err := s.Client.CreateBucket(bucketInput) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | if infoLogging { 108 | klog.InfoS("successfully created bucket", "name", name) 109 | } else { 110 | klog.InfoS("successfully created bucket", "name", name) 111 | } 112 | return nil 113 | } 114 | 115 | // DeleteBucket function deletes given bucket using s3 client 116 | func (s *S3Agent) DeleteBucket(name string) (bool, error) { 117 | _, err := s.Client.DeleteBucket(&s3.DeleteBucketInput{ 118 | Bucket: aws.String(name), 119 | }) 120 | if err != nil { 121 | klog.ErrorS(err, "failed to delete bucket") 122 | return false, err 123 | 124 | } 125 | return true, nil 126 | } 127 | 128 | // PutObjectInBucket function puts an object in a bucket using s3 client 129 | func (s *S3Agent) PutObjectInBucket(bucketname string, body string, key string, 130 | contentType string) (bool, error) { 131 | _, err := s.Client.PutObject(&s3.PutObjectInput{ 132 | Body: strings.NewReader(body), 133 | Bucket: &bucketname, 134 | Key: &key, 135 | ContentType: &contentType, 136 | }) 137 | if err != nil { 138 | klog.ErrorS(err, "failed to put object in bucket") 139 | return false, err 140 | 141 | } 142 | return true, nil 143 | } 144 | 145 | // GetObjectInBucket function retrieves an object from a bucket using s3 client 146 | func (s *S3Agent) GetObjectInBucket(bucketname string, key string) (string, error) { 147 | result, err := s.Client.GetObject(&s3.GetObjectInput{ 148 | Bucket: aws.String(bucketname), 149 | Key: aws.String(key), 150 | }) 151 | 152 | if err != nil { 153 | klog.ErrorS(err, "failed to retrieve object from bucket") 154 | return "ERROR_ OBJECT NOT FOUND", err 155 | 156 | } 157 | buf := new(bytes.Buffer) 158 | _, err = buf.ReadFrom(result.Body) 159 | if err != nil { 160 | return "", err 161 | } 162 | 163 | return buf.String(), nil 164 | } 165 | 166 | // DeleteObjectInBucket function deletes given bucket using s3 client 167 | func (s *S3Agent) DeleteObjectInBucket(bucketname string, key string) (bool, error) { 168 | _, err := s.Client.DeleteObject(&s3.DeleteObjectInput{ 169 | Bucket: aws.String(bucketname), 170 | Key: aws.String(key), 171 | }) 172 | if err != nil { 173 | if aerr, ok := err.(awserr.Error); ok { 174 | switch aerr.Code() { 175 | case s3.ErrCodeNoSuchBucket: 176 | return true, nil 177 | case s3.ErrCodeNoSuchKey: 178 | return true, nil 179 | } 180 | } 181 | klog.ErrorS(err, "failed to delete object from bucket") 182 | return false, err 183 | 184 | } 185 | return true, nil 186 | } 187 | 188 | func buildTransportTLS(tlsCert []byte, insecure bool) *http.Transport { 189 | //nolint:gosec // is enabled only for testing 190 | tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12, InsecureSkipVerify: insecure} 191 | if len(tlsCert) > 0 { 192 | caCertPool := x509.NewCertPool() 193 | caCertPool.AppendCertsFromPEM(tlsCert) 194 | tlsConfig.RootCAs = caCertPool 195 | } 196 | 197 | return &http.Transport{ 198 | TLSClientConfig: tlsConfig, 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 The Ceph COSI Authors. All rights reserved. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License 202 | -------------------------------------------------------------------------------- /pkg/util/s3client/policy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Ceph-COSI Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | You may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package s3client 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/aws/aws-sdk-go/service/s3" 22 | "k8s.io/apimachinery/pkg/util/json" 23 | ) 24 | 25 | type action string 26 | 27 | const ( 28 | All action = "s3:*" 29 | AbortMultipartUpload action = "s3:AbortMultipartUpload" 30 | CreateBucket action = "s3:CreateBucket" 31 | DeleteBucketPolicy action = "s3:DeleteBucketPolicy" 32 | DeleteBucket action = "s3:DeleteBucket" 33 | DeleteBucketWebsite action = "s3:DeleteBucketWebsite" 34 | DeleteObject action = "s3:DeleteObject" 35 | DeleteObjectVersion action = "s3:DeleteObjectVersion" 36 | DeleteReplicationConfiguration action = "s3:DeleteReplicationConfiguration" 37 | GetAccelerateConfiguration action = "s3:GetAccelerateConfiguration" 38 | GetBucketAcl action = "s3:GetBucketAcl" 39 | GetBucketCORS action = "s3:GetBucketCORS" 40 | GetBucketLocation action = "s3:GetBucketLocation" 41 | GetBucketLogging action = "s3:GetBucketLogging" 42 | GetBucketNotification action = "s3:GetBucketNotification" 43 | GetBucketPolicy action = "s3:GetBucketPolicy" 44 | GetBucketRequestPayment action = "s3:GetBucketRequestPayment" 45 | GetBucketTagging action = "s3:GetBucketTagging" 46 | GetBucketVersioning action = "s3:GetBucketVersioning" 47 | GetBucketWebsite action = "s3:GetBucketWebsite" 48 | GetLifecycleConfiguration action = "s3:GetLifecycleConfiguration" 49 | GetObjectAcl action = "s3:GetObjectAcl" 50 | GetObject action = "s3:GetObject" 51 | GetObjectTorrent action = "s3:GetObjectTorrent" 52 | GetObjectVersionAcl action = "s3:GetObjectVersionAcl" 53 | GetObjectVersion action = "s3:GetObjectVersion" 54 | GetObjectVersionTorrent action = "s3:GetObjectVersionTorrent" 55 | GetReplicationConfiguration action = "s3:GetReplicationConfiguration" 56 | ListAllMyBuckets action = "s3:ListAllMyBuckets" 57 | ListBucketMultiPartUploads action = "s3:ListBucketMultiPartUploads" 58 | ListBucket action = "s3:ListBucket" 59 | ListBucketVersions action = "s3:ListBucketVersions" 60 | ListMultipartUploadParts action = "s3:ListMultipartUploadParts" 61 | PutAccelerateConfiguration action = "s3:PutAccelerateConfiguration" 62 | PutBucketAcl action = "s3:PutBucketAcl" 63 | PutBucketCORS action = "s3:PutBucketCORS" 64 | PutBucketLogging action = "s3:PutBucketLogging" 65 | PutBucketNotification action = "s3:PutBucketNotification" 66 | PutBucketPolicy action = "s3:PutBucketPolicy" 67 | PutBucketRequestPayment action = "s3:PutBucketRequestPayment" 68 | PutBucketTagging action = "s3:PutBucketTagging" 69 | PutBucketVersioning action = "s3:PutBucketVersioning" 70 | PutBucketWebsite action = "s3:PutBucketWebsite" 71 | PutLifecycleConfiguration action = "s3:PutLifecycleConfiguration" 72 | PutObjectAcl action = "s3:PutObjectAcl" 73 | PutObject action = "s3:PutObject" 74 | PutObjectVersionAcl action = "s3:PutObjectVersionAcl" 75 | PutReplicationConfiguration action = "s3:PutReplicationConfiguration" 76 | RestoreObject action = "s3:RestoreObject" 77 | ) 78 | 79 | // AllowedActions is a lenient default list of actions 80 | var AllowedActions = []action{ 81 | DeleteObject, 82 | DeleteObjectVersion, 83 | GetBucketAcl, 84 | GetBucketCORS, 85 | GetBucketLocation, 86 | GetBucketLogging, 87 | GetBucketNotification, 88 | GetBucketTagging, 89 | GetBucketVersioning, 90 | GetBucketWebsite, 91 | GetObject, 92 | GetObjectAcl, 93 | GetObjectTorrent, 94 | GetObjectVersion, 95 | GetObjectVersionAcl, 96 | GetObjectVersionTorrent, 97 | ListAllMyBuckets, 98 | ListBucket, 99 | ListBucketMultiPartUploads, 100 | ListBucketVersions, 101 | ListMultipartUploadParts, 102 | PutBucketTagging, 103 | PutBucketVersioning, 104 | PutBucketWebsite, 105 | PutBucketVersioning, 106 | PutLifecycleConfiguration, 107 | PutObject, 108 | PutObjectAcl, 109 | PutObjectVersionAcl, 110 | PutReplicationConfiguration, 111 | RestoreObject, 112 | } 113 | 114 | type effect string 115 | 116 | // effectAllow and effectDeny values are expected by the S3 API to be 'Allow' or 'Deny' explicitly 117 | const ( 118 | effectAllow effect = "Allow" 119 | effectDeny effect = "Deny" 120 | ) 121 | 122 | // PolicyStatment is the Go representation of a PolicyStatement json struct 123 | // it defines what Actions that a Principle can or cannot perform on a Resource 124 | type PolicyStatement struct { 125 | // Sid (optional) is the PolicyStatement's unique identifier 126 | Sid string `json:"Sid"` 127 | // Effect determines whether the Action(s) are 'Allow'ed or 'Deny'ed. 128 | Effect effect `json:"Effect"` 129 | // Principle is/are the Ceph user names affected by this PolicyStatement 130 | // Must be in the format of 'arn:aws:iam:::user/' 131 | Principal map[string][]string `json:"Principal"` 132 | // Action is a list of s3:* actions 133 | Action []action `json:"Action"` 134 | // Resource is the ARN identifier for the S3 resource (bucket) 135 | // Must be in the format of 'arn:aws:s3:::' 136 | Resource []string `json:"Resource"` 137 | } 138 | 139 | // BucketPolicy represents set of policy statements for a single bucket. 140 | type BucketPolicy struct { 141 | // Id (optional) identifies the bucket policy 142 | Id string `json:"Id"` 143 | // Version is the version of the BucketPolicy data structure 144 | // should always be '2012-10-17' 145 | Version string `json:"Version"` 146 | Statement []PolicyStatement `json:"Statement"` 147 | } 148 | 149 | // the version of the BucketPolicy json structure 150 | const version = "2012-10-17" 151 | 152 | // NewBucketPolicy obviously returns a new BucketPolicy. PolicyStatements may be passed in at creation 153 | // or added after the fact. BucketPolicies should be passed to PutBucketPolicy(). 154 | func NewBucketPolicy(ps ...PolicyStatement) *BucketPolicy { 155 | bp := &BucketPolicy{ 156 | Version: version, 157 | Statement: append([]PolicyStatement{}, ps...), 158 | } 159 | return bp 160 | } 161 | 162 | // PutBucketPolicy applies the policy to the bucket 163 | func (s *S3Agent) PutBucketPolicy(bucket string, policy BucketPolicy) (*s3.PutBucketPolicyOutput, error) { 164 | 165 | confirmRemoveSelfBucketAccess := false 166 | serializedPolicy, _ := json.Marshal(policy) 167 | consumablePolicy := string(serializedPolicy) 168 | 169 | p := &s3.PutBucketPolicyInput{ 170 | Bucket: &bucket, 171 | ConfirmRemoveSelfBucketAccess: &confirmRemoveSelfBucketAccess, 172 | Policy: &consumablePolicy, 173 | } 174 | out, err := s.Client.PutBucketPolicy(p) 175 | if err != nil { 176 | return out, err 177 | } 178 | return out, nil 179 | } 180 | 181 | func (s *S3Agent) GetBucketPolicy(bucket string) (*BucketPolicy, error) { 182 | out, err := s.Client.GetBucketPolicy(&s3.GetBucketPolicyInput{ 183 | Bucket: &bucket, 184 | }) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | policy := &BucketPolicy{} 190 | err = json.Unmarshal([]byte(*out.Policy), policy) 191 | if err != nil { 192 | return nil, err 193 | } 194 | return policy, nil 195 | } 196 | 197 | // ModifyBucketPolicy new and old statement SIDs and overwrites on a match. 198 | // This allows users to Get, modify, and Replace existing statements as well as 199 | // add new ones. 200 | func (bp *BucketPolicy) ModifyBucketPolicy(ps ...PolicyStatement) *BucketPolicy { 201 | for _, newP := range ps { 202 | var match bool 203 | for j, oldP := range bp.Statement { 204 | if newP.Sid == oldP.Sid { 205 | bp.Statement[j] = newP 206 | } 207 | } 208 | if !match { 209 | bp.Statement = append(bp.Statement, newP) 210 | } 211 | } 212 | return bp 213 | } 214 | 215 | func (bp *BucketPolicy) DropPolicyStatements(sid ...string) *BucketPolicy { 216 | for _, s := range sid { 217 | for i, stmt := range bp.Statement { 218 | if stmt.Sid == s { 219 | bp.Statement = append(bp.Statement[:i], bp.Statement[i+1:]...) 220 | break 221 | } 222 | } 223 | } 224 | return bp 225 | } 226 | 227 | func (bp *BucketPolicy) EjectPrincipals(users ...string) *BucketPolicy { 228 | statements := bp.Statement 229 | for _, s := range statements { 230 | s.EjectPrincipals(users...) 231 | } 232 | bp.Statement = statements 233 | return bp 234 | } 235 | 236 | // NewPolicyStatement generates a new PolicyStatement. PolicyStatment methods are designed to 237 | // be chain called with dot notation to allow for easy configuration at creation. This is preferable 238 | // to a long parameter list. 239 | func NewPolicyStatement() *PolicyStatement { 240 | return &PolicyStatement{ 241 | Sid: "", 242 | Effect: "", 243 | Principal: map[string][]string{}, 244 | Action: []action{}, 245 | Resource: []string{}, 246 | } 247 | } 248 | 249 | func (ps *PolicyStatement) WithSID(sid string) *PolicyStatement { 250 | ps.Sid = sid 251 | return ps 252 | } 253 | 254 | const awsPrinciple = "AWS" 255 | const arnPrefixPrinciple = "arn:aws:iam:::user/%s" 256 | const arnPrefixResource = "arn:aws:s3:::%s" 257 | 258 | // ForPrincipals adds users to the PolicyStatement 259 | func (ps *PolicyStatement) ForPrincipals(users ...string) *PolicyStatement { 260 | principals := ps.Principal[awsPrinciple] 261 | for _, u := range users { 262 | principals = append(principals, fmt.Sprintf(arnPrefixPrinciple, u)) 263 | } 264 | ps.Principal[awsPrinciple] = principals 265 | return ps 266 | } 267 | 268 | // ForResources adds resources (buckets) to the PolicyStatement with the appropriate ARN prefix 269 | func (ps *PolicyStatement) ForResources(resources ...string) *PolicyStatement { 270 | for _, v := range resources { 271 | ps.Resource = append(ps.Resource, fmt.Sprintf(arnPrefixResource, v)) 272 | } 273 | return ps 274 | } 275 | 276 | // ForSubResources add contents inside the bucket to the PolicyStatement with the appropriate ARN prefix 277 | func (ps *PolicyStatement) ForSubResources(resources ...string) *PolicyStatement { 278 | var subresource string 279 | for _, v := range resources { 280 | subresource = fmt.Sprintf("%s/*", v) 281 | ps.Resource = append(ps.Resource, fmt.Sprintf(arnPrefixResource, subresource)) 282 | } 283 | return ps 284 | } 285 | 286 | // Allows sets the effect of the PolicyStatement to allow PolicyStatement's Actions 287 | func (ps *PolicyStatement) Allows() *PolicyStatement { 288 | if ps.Effect != "" { 289 | return ps 290 | } 291 | ps.Effect = effectAllow 292 | return ps 293 | } 294 | 295 | // Denies sets the effect of the PolicyStatement to deny the PolicyStatement's Actions 296 | func (ps *PolicyStatement) Denies() *PolicyStatement { 297 | if ps.Effect != "" { 298 | return ps 299 | } 300 | ps.Effect = effectDeny 301 | return ps 302 | } 303 | 304 | // Actions is the set of "s3:*" actions for the PolicyStatement is concerned 305 | func (ps *PolicyStatement) Actions(actions ...action) *PolicyStatement { 306 | ps.Action = actions 307 | return ps 308 | } 309 | 310 | func (ps *PolicyStatement) EjectPrincipals(users ...string) { 311 | principals := ps.Principal[awsPrinciple] 312 | for _, u := range users { 313 | for j, v := range principals { 314 | if u == v { 315 | principals = append(principals[:j], principals[:j+1]...) 316 | } 317 | } 318 | } 319 | ps.Principal[awsPrinciple] = principals 320 | } 321 | -------------------------------------------------------------------------------- /pkg/driver/provisioner.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Ceph-COSI Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | You may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package driver 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "os" 22 | 23 | "github.com/ceph/cosi-driver-ceph/pkg/util/s3client" 24 | 25 | "github.com/aws/aws-sdk-go/aws/awserr" 26 | "github.com/aws/aws-sdk-go/service/s3" 27 | rgwadmin "github.com/ceph/go-ceph/rgw/admin" 28 | "google.golang.org/grpc/codes" 29 | "google.golang.org/grpc/status" 30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 | "k8s.io/client-go/kubernetes" 32 | "k8s.io/client-go/rest" 33 | "k8s.io/klog/v2" 34 | bucketclientset "sigs.k8s.io/container-object-storage-interface/client/clientset/versioned" 35 | cosispec "sigs.k8s.io/container-object-storage-interface/proto" 36 | ) 37 | 38 | // contains two clients 39 | // 1.) for RGWAdminOps : mainly for user related operations 40 | // 2.) for S3 operations : mainly for bucket related operations 41 | type provisionerServer struct { 42 | cosispec.UnimplementedProvisionerServer 43 | Provisioner string 44 | Clientset *kubernetes.Clientset 45 | KubeConfig *rest.Config 46 | BucketClientset bucketclientset.Interface 47 | } 48 | 49 | var _ cosispec.ProvisionerServer = &provisionerServer{} 50 | 51 | var initializeClients = InitializeClients 52 | 53 | func NewProvisionerServer(provisioner string) (cosispec.ProvisionerServer, error) { 54 | kubeConfig, err := rest.InClusterConfig() 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | clientset, err := kubernetes.NewForConfig(kubeConfig) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | bucketClientset, err := bucketclientset.NewForConfig(kubeConfig) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return &provisionerServer{ 70 | Provisioner: provisioner, 71 | Clientset: clientset, 72 | KubeConfig: kubeConfig, 73 | BucketClientset: bucketClientset, 74 | }, nil 75 | } 76 | 77 | // ProvisionerCreateBucket is an idempotent method for creating buckets 78 | // It is expected to create the same bucket given a bucketName and protocol 79 | // If the bucket already exists, then it MUST return codes.AlreadyExists 80 | // Return values 81 | // 82 | // nil - Bucket successfully created 83 | // codes.AlreadyExists - Bucket already exists. No more retries 84 | // non-nil err - Internal error [requeue'd with exponential backoff] 85 | func (s *provisionerServer) DriverCreateBucket(ctx context.Context, 86 | req *cosispec.DriverCreateBucketRequest) (*cosispec.DriverCreateBucketResponse, error) { 87 | klog.V(5).Infof("req %v", req) 88 | 89 | bucketName := req.GetName() 90 | klog.V(3).InfoS("Creating Bucket", "name", bucketName) 91 | 92 | parameters := req.GetParameters() 93 | 94 | s3Client, _, err := initializeClients(ctx, s.Clientset, parameters) 95 | if err != nil { 96 | klog.ErrorS(err, "failed to initialize clients") 97 | return nil, status.Error(codes.Internal, "failed to initialize clients") 98 | } 99 | 100 | err = s3Client.CreateBucket(bucketName) 101 | if err != nil { 102 | if aerr, ok := err.(awserr.Error); ok { 103 | klog.InfoS("DEBUG: after s3 call", "ok", ok, "aerr", aerr) 104 | switch aerr.Code() { 105 | case s3.ErrCodeBucketAlreadyExists: 106 | klog.InfoS("bucket already exists", "name", bucketName) 107 | return nil, status.Error(codes.AlreadyExists, "bucket already exists") 108 | case s3.ErrCodeBucketAlreadyOwnedByYou: 109 | klog.InfoS("bucket already owned by you", "name", bucketName) 110 | return nil, status.Error(codes.AlreadyExists, "bucket already owned by you") 111 | } 112 | } 113 | klog.ErrorS(err, "failed to create bucket", "bucketName", bucketName) 114 | return nil, status.Error(codes.Internal, "failed to create bucket") 115 | } 116 | klog.InfoS("Successfully created Backend Bucket", "bucketName", bucketName) 117 | 118 | return &cosispec.DriverCreateBucketResponse{ 119 | BucketId: bucketName, 120 | }, nil 121 | } 122 | 123 | func (s *provisionerServer) DriverDeleteBucket(ctx context.Context, 124 | req *cosispec.DriverDeleteBucketRequest) (*cosispec.DriverDeleteBucketResponse, error) { 125 | klog.V(5).Infof("req %v", req) 126 | bucketName := req.GetBucketId() 127 | klog.V(3).InfoS("Deleting Bucket", "name", bucketName) 128 | bucket, err := s.BucketClientset.ObjectstorageV1alpha1().Buckets().Get(ctx, bucketName, metav1.GetOptions{}) 129 | if err != nil { 130 | klog.ErrorS(err, "failed to get bucket", "bucketName", bucketName) 131 | return nil, status.Error(codes.Internal, "failed to get bucket") 132 | } 133 | 134 | parameters := bucket.Spec.Parameters 135 | s3Client, _, err := initializeClients(ctx, s.Clientset, parameters) 136 | if err != nil { 137 | klog.ErrorS(err, "failed to initialize clients") 138 | return nil, status.Error(codes.Internal, "failed to initialize clients") 139 | } 140 | 141 | _, err = s3Client.DeleteBucket(bucketName) 142 | if err != nil { 143 | klog.ErrorS(err, "failed to delete bucket", "bucketName", bucketName) 144 | return nil, status.Error(codes.Internal, "failed to delete bucket") 145 | } 146 | klog.InfoS("Successfully deleted Backend Bucket", "bucketName", bucketName) 147 | return &cosispec.DriverDeleteBucketResponse{}, nil 148 | } 149 | 150 | func (s *provisionerServer) DriverGrantBucketAccess(ctx context.Context, 151 | req *cosispec.DriverGrantBucketAccessRequest) (*cosispec.DriverGrantBucketAccessResponse, error) { 152 | // TODO : validate below details, Authenticationtype, Parameters 153 | userName := req.GetName() 154 | bucketName := req.GetBucketId() 155 | klog.V(5).Infof("req %v", req) 156 | klog.Info("Granting user accessPolicy to bucket ", "userName", userName, "bucketName", bucketName) 157 | parameters := req.GetParameters() 158 | 159 | s3Client, rgwAdminClient, err := initializeClients(ctx, s.Clientset, parameters) 160 | if err != nil { 161 | klog.ErrorS(err, "failed to initialize clients") 162 | return nil, status.Error(codes.Internal, "failed to initialize clients") 163 | } 164 | 165 | user, err := rgwAdminClient.CreateUser(ctx, rgwadmin.User{ 166 | ID: userName, 167 | DisplayName: userName, 168 | }) 169 | 170 | // TODO : Do we need fail for UserErrorExists, or same account can have multiple BAR 171 | if err != nil && !errors.Is(err, rgwadmin.ErrUserExists) { 172 | klog.ErrorS(err, "failed to create user") 173 | return nil, status.Error(codes.Internal, "User creation failed") 174 | } 175 | 176 | policy, err := s3Client.GetBucketPolicy(bucketName) 177 | if err != nil { 178 | if aerr, ok := err.(awserr.Error); ok && aerr.Code() != "NoSuchBucketPolicy" { 179 | return nil, status.Error(codes.Internal, "fetching policy failed") 180 | } 181 | } 182 | 183 | statement := s3client.NewPolicyStatement(). 184 | WithSID(userName). 185 | ForPrincipals(userName). 186 | ForResources(bucketName). 187 | ForSubResources(bucketName). 188 | Allows(). 189 | Actions(s3client.AllowedActions...) 190 | if policy == nil { 191 | policy = s3client.NewBucketPolicy(*statement) 192 | } else { 193 | policy = policy.ModifyBucketPolicy(*statement) 194 | } 195 | _, err = s3Client.PutBucketPolicy(bucketName, *policy) 196 | if err != nil { 197 | klog.ErrorS(err, "failed to set policy") 198 | return nil, status.Error(codes.Internal, "failed to set policy") 199 | } 200 | 201 | // TODO : limit the bucket count for this user to 0 202 | 203 | // Below response if not final, may change in future 204 | return &cosispec.DriverGrantBucketAccessResponse{ 205 | AccountId: userName, 206 | Credentials: fetchUserCredentials(user, rgwAdminClient.Endpoint, ""), 207 | }, nil 208 | } 209 | 210 | func (s *provisionerServer) DriverRevokeBucketAccess(ctx context.Context, 211 | req *cosispec.DriverRevokeBucketAccessRequest) (*cosispec.DriverRevokeBucketAccessResponse, error) { 212 | klog.V(5).Infof("req %v", req) 213 | bucketName := req.GetBucketId() 214 | bucket, err := s.BucketClientset.ObjectstorageV1alpha1().Buckets().Get(ctx, bucketName, metav1.GetOptions{}) 215 | if err != nil { 216 | klog.ErrorS(err, "failed to get bucket", "bucketName", bucketName) 217 | return nil, status.Error(codes.Internal, "failed to get bucket") 218 | } 219 | 220 | parameters := bucket.Spec.Parameters 221 | _, rgwAdminClient, err := initializeClients(ctx, s.Clientset, parameters) 222 | if err != nil { 223 | klog.ErrorS(err, "failed to initialize clients") 224 | return nil, status.Error(codes.Internal, "failed to initialize clients") 225 | } 226 | 227 | userName := req.GetAccountId() 228 | 229 | // TODO : instead of deleting user, revoke its permission and delete only if no more bucket attached to it 230 | err = rgwAdminClient.RemoveUser(ctx, rgwadmin.User{ID: userName}) 231 | if err != nil { 232 | klog.ErrorS(err, "failed to delete user") 233 | return nil, status.Error(codes.Internal, "failed to delete user") 234 | } 235 | return &cosispec.DriverRevokeBucketAccessResponse{}, nil 236 | } 237 | 238 | func fetchUserCredentials(user rgwadmin.User, endpoint string, region string) map[string]*cosispec.CredentialDetails { 239 | s3Keys := make(map[string]string) 240 | s3Keys["accessKeyID"] = user.Keys[0].AccessKey 241 | s3Keys["accessSecretKey"] = user.Keys[0].SecretKey 242 | s3Keys["endpoint"] = endpoint 243 | s3Keys["region"] = region 244 | creds := &cosispec.CredentialDetails{ 245 | Secrets: s3Keys, 246 | } 247 | credDetails := make(map[string]*cosispec.CredentialDetails) 248 | credDetails["s3"] = creds 249 | return credDetails 250 | } 251 | 252 | func InitializeClients(ctx context.Context, clientset *kubernetes.Clientset, parameters map[string]string) (*s3client.S3Agent, *rgwadmin.API, error) { 253 | klog.V(5).Infof("Initializing clients %v", parameters) 254 | 255 | objectStoreUserSecretName, namespace, err := fetchSecretNameAndNamespace(parameters) 256 | if err != nil { 257 | return nil, nil, err 258 | } 259 | 260 | objectStoreUserSecret, err := clientset.CoreV1().Secrets(namespace).Get(ctx, objectStoreUserSecretName, metav1.GetOptions{}) 261 | if err != nil { 262 | klog.ErrorS(err, "failed to get object store user secret") 263 | return nil, nil, status.Error(codes.Internal, "failed to get object store user secret") 264 | } 265 | 266 | accessKey, secretKey, rgwEndpoint, _, err := fetchParameters(objectStoreUserSecret.Data) 267 | if err != nil { 268 | return nil, nil, err 269 | } 270 | 271 | // TODO : validate endpoint and support TLS certs 272 | 273 | rgwAdminClient, err := rgwadmin.New(rgwEndpoint, accessKey, secretKey, nil) 274 | if err != nil { 275 | klog.ErrorS(err, "failed to create rgw admin client") 276 | return nil, nil, status.Error(codes.Internal, "failed to create rgw admin client") 277 | } 278 | s3Client, err := s3client.NewS3Agent(accessKey, secretKey, rgwEndpoint, nil, true) 279 | if err != nil { 280 | klog.ErrorS(err, "failed to create s3 client") 281 | return nil, nil, status.Error(codes.Internal, "failed to create s3 client") 282 | } 283 | return s3Client, rgwAdminClient, nil 284 | } 285 | 286 | func fetchParameters(secretData map[string][]byte) (string, string, string, string, error) { 287 | accessKey := string(secretData["AccessKey"]) 288 | secretKey := string(secretData["SecretKey"]) 289 | endPoint := string(secretData["Endpoint"]) 290 | if endPoint == "" || accessKey == "" || secretKey == "" { 291 | return "", "", "", "", status.Error(codes.InvalidArgument, "endpoint, accessKeyID and secretKey are required") 292 | } 293 | tlsCert := string(secretData["SSLCertSecretName"]) 294 | 295 | return accessKey, secretKey, endPoint, tlsCert, nil 296 | } 297 | 298 | func fetchSecretNameAndNamespace(parameters map[string]string) (string, string, error) { 299 | secretName := parameters["objectStoreUserSecretName"] 300 | namespace := os.Getenv("POD_NAMESPACE") 301 | if parameters["objectStoreUserSecretNamespace"] != "" { 302 | namespace = parameters["objectStoreUserSecretNamespace"] 303 | } 304 | if secretName == "" || namespace == "" { 305 | return "", "", status.Error(codes.InvalidArgument, "objectStoreUserSecretName and Namespace is required") 306 | } 307 | 308 | return secretName, namespace, nil 309 | } 310 | -------------------------------------------------------------------------------- /pkg/driver/provisioner_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021 The Ceph-COSI Authors. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | You may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | package driver 17 | 18 | import ( 19 | "bytes" 20 | "context" 21 | "encoding/json" 22 | "fmt" 23 | "io" 24 | "net/http" 25 | "reflect" 26 | "testing" 27 | 28 | s3cli "github.com/ceph/cosi-driver-ceph/pkg/util/s3client" 29 | 30 | rgwadmin "github.com/ceph/go-ceph/rgw/admin" 31 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 | "k8s.io/client-go/kubernetes" 33 | "sigs.k8s.io/container-object-storage-interface/client/apis/objectstorage/v1alpha1" 34 | fakebucketclientset "sigs.k8s.io/container-object-storage-interface/client/clientset/versioned/fake" 35 | cosispec "sigs.k8s.io/container-object-storage-interface/proto" 36 | ) 37 | 38 | const ( 39 | userCreateJSON = `{ 40 | "user_id": "test-user", 41 | "display_name": "test-user", 42 | "email": "", 43 | "suspended": 0, 44 | "max_buckets": 1000, 45 | "subusers": [], 46 | "keys": [ 47 | { 48 | "user": "test-user", 49 | "access_key": "AccessKey", 50 | "secret_key": "SecretKey" 51 | } 52 | ], 53 | "swift_keys": [], 54 | "caps": [ 55 | { 56 | "type": "users", 57 | "perm": "*" 58 | } 59 | ], 60 | "op_mask": "read, write, delete", 61 | "default_placement": "", 62 | "default_storage_class": "", 63 | "placement_tags": [], 64 | "bucket_quota": { 65 | "enabled": false, 66 | "check_on_raw": false, 67 | "max_size": -1, 68 | "max_size_kb": 0, 69 | "max_objects": -1 70 | }, 71 | "user_quota": { 72 | "enabled": false, 73 | "check_on_raw": false, 74 | "max_size": -1, 75 | "max_size_kb": 0, 76 | "max_objects": -1 77 | }, 78 | "temp_url_keys": [], 79 | "type": "rgw", 80 | "mfa_ids": [] 81 | }` 82 | ) 83 | 84 | func createParameters() map[string]string { 85 | return map[string]string{ 86 | "objectStoreUserSecretName": "test-user-secret", 87 | "objectStoreUserSecretNamespace": "test-namespace", 88 | } 89 | } 90 | func Test_provisionerServer_DriverCreateBucket(t *testing.T) { 91 | type fields struct { 92 | provisioner string 93 | } 94 | 95 | type args struct { 96 | ctx context.Context 97 | req *cosispec.DriverCreateBucketRequest 98 | } 99 | 100 | initializeClients = func(ctx context.Context, clientset *kubernetes.Clientset, parameters map[string]string) (*s3cli.S3Agent, *rgwadmin.API, error) { 101 | _, _, err := fetchSecretNameAndNamespace(parameters) 102 | if err != nil { 103 | t.Fatalf("failed to fetch secret name and namespace: %v", err) 104 | } 105 | s3Client := &s3cli.S3Agent{ 106 | Client: mockS3Client{}, 107 | } 108 | return s3Client, nil, nil 109 | } 110 | 111 | tests := []struct { 112 | name string 113 | fields fields 114 | args args 115 | want *cosispec.DriverCreateBucketResponse 116 | wantErr bool 117 | }{ 118 | {"Empty Bucket Name", fields{"CreateBucket Empty Bucket Name"}, args{context.Background(), &cosispec.DriverCreateBucketRequest{Name: "", Parameters: createParameters()}}, nil, true}, 119 | {"Create Bucket success", fields{"CreateBucket Success"}, args{context.Background(), &cosispec.DriverCreateBucketRequest{Name: "test-bucket", Parameters: createParameters()}}, &cosispec.DriverCreateBucketResponse{BucketId: "test-bucket"}, false}, 120 | {"Create Bucket failure", fields{"CreateBucket Failure"}, args{context.Background(), &cosispec.DriverCreateBucketRequest{Name: "failed-bucket", Parameters: createParameters()}}, nil, true}, 121 | {"Bucket already Exists", fields{"CreateBucket Already Exists"}, args{context.Background(), &cosispec.DriverCreateBucketRequest{Name: "test-bucket-already-exists", Parameters: createParameters()}}, nil, true}, 122 | {"Bucket owned same user", fields{"CreateBucket Owned by same user"}, args{context.Background(), &cosispec.DriverCreateBucketRequest{Name: "test-bucket-owned-by-same-user", Parameters: createParameters()}}, nil, true}, 123 | } 124 | for _, tt := range tests { 125 | t.Run(tt.name, func(t *testing.T) { 126 | s := &provisionerServer{ 127 | Provisioner: tt.fields.provisioner, 128 | } 129 | got, err := s.DriverCreateBucket(tt.args.ctx, tt.args.req) 130 | if (err != nil) != tt.wantErr { 131 | t.Errorf("provisionerServer.DriverCreateBucket() error = %v, wantErr %v", err, tt.wantErr) 132 | return 133 | } 134 | if !reflect.DeepEqual(got, tt.want) { 135 | t.Errorf("provisionerServer.DriverCreateBucket() = %v, want %v", got, tt.want) 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func Test_provisionerServer_DriverGrantBucketAccess(t *testing.T) { 142 | type fields struct { 143 | provisioner string 144 | } 145 | type args struct { 146 | ctx context.Context 147 | req *cosispec.DriverGrantBucketAccessRequest 148 | } 149 | initializeClients = func(ctx context.Context, clientset *kubernetes.Clientset, parameters map[string]string) (*s3cli.S3Agent, *rgwadmin.API, error) { 150 | _, _, err := fetchSecretNameAndNamespace(parameters) 151 | if err != nil { 152 | t.Fatalf("failed to fetch secret name and namespace: %v", err) 153 | } 154 | 155 | s3Client := &s3cli.S3Agent{ 156 | Client: mockS3Client{}, 157 | } 158 | mockClient := &MockClient{ 159 | MockDo: func(req *http.Request) (*http.Response, error) { 160 | if req.Method == http.MethodPut { 161 | if req.URL.RawQuery == "display-name=test-user&format=json&uid=test-user" { 162 | return &http.Response{ 163 | StatusCode: 200, 164 | Body: io.NopCloser(bytes.NewReader([]byte(userCreateJSON))), 165 | }, nil 166 | } 167 | } 168 | return nil, fmt.Errorf("unexpected request: %q. method %q. path %q", req.URL.RawQuery, req.Method, req.URL.Path) 169 | }, 170 | } 171 | rgwAdminClient, err := rgwadmin.New("rgw-my-store:8000", "accesskey", "secretkey", mockClient) 172 | if err != nil { 173 | t.Fatalf("failed to create rgw admin client: %v", err) 174 | } 175 | return s3Client, rgwAdminClient, nil 176 | } 177 | u := rgwadmin.User{} 178 | err := json.Unmarshal([]byte(userCreateJSON), &u) 179 | if err != nil { 180 | t.Fatalf("failed to unmarshal user create json: %v", err) 181 | } 182 | tests := []struct { 183 | name string 184 | fields fields 185 | args args 186 | want *cosispec.DriverGrantBucketAccessResponse 187 | wantErr bool 188 | }{ 189 | {"Empty Bucket Name", fields{"GrantBucketAccess Empty Bucket Name"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "", Name: "test-user", Parameters: createParameters()}}, nil, true}, 190 | {"Empty User Name", fields{"GrantBucketAccess Empty User Name"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "test-bucket", Name: "", Parameters: createParameters()}}, nil, true}, 191 | {"Grant Bucket Access success", fields{"GrantBucketAccess Success"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "test-bucket", Name: "test-user", Parameters: createParameters()}}, &cosispec.DriverGrantBucketAccessResponse{AccountId: "test-user", Credentials: fetchUserCredentials(u, "rgw-my-store:8000", "")}, false}, 192 | {"Grant Bucket Access failure", fields{"GrantBucketAccess Failure"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "failed-bucket", Name: "test-user", Parameters: createParameters()}}, nil, true}, 193 | {"Bucket does not exist", fields{"GrantBucketAccess Does not exist"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "test-bucket-does-not-exist", Name: "test-user", Parameters: createParameters()}}, nil, true}, 194 | {"User does not exist", fields{"GrantBucketAccess User Does not exist"}, args{context.Background(), &cosispec.DriverGrantBucketAccessRequest{BucketId: "test-bucket", Name: "test-user-does-not-exist", Parameters: createParameters()}}, nil, true}, 195 | } 196 | for _, tt := range tests { 197 | t.Run(tt.name, func(t *testing.T) { 198 | s := &provisionerServer{ 199 | Provisioner: tt.fields.provisioner, 200 | } 201 | got, err := s.DriverGrantBucketAccess(tt.args.ctx, tt.args.req) 202 | if (err != nil) != tt.wantErr { 203 | t.Errorf("provisionerServer.DriverGrantBucketAccess() error = %v, wantErr %v", err, tt.wantErr) 204 | return 205 | } 206 | if !reflect.DeepEqual(got, tt.want) { 207 | t.Errorf("provisionerServer.DriverGrantBucketAccess() = %v, want %v", got, tt.want) 208 | } 209 | }) 210 | } 211 | } 212 | 213 | func Test_provisionerServer_DriverDeleteBucket(t *testing.T) { 214 | type fields struct { 215 | provisioner string 216 | } 217 | 218 | type args struct { 219 | ctx context.Context 220 | req *cosispec.DriverDeleteBucketRequest 221 | } 222 | 223 | initializeClients = func(ctx context.Context, clientset *kubernetes.Clientset, parameters map[string]string) (*s3cli.S3Agent, *rgwadmin.API, error) { 224 | _, _, err := fetchSecretNameAndNamespace(parameters) 225 | if err != nil { 226 | t.Fatalf("failed to fetch secret name and namespace: %v", err) 227 | } 228 | s3Client := &s3cli.S3Agent{ 229 | Client: mockS3Client{}, 230 | } 231 | return s3Client, nil, nil 232 | } 233 | 234 | tests := []struct { 235 | name string 236 | fields fields 237 | args args 238 | want *cosispec.DriverDeleteBucketResponse 239 | wantErr bool 240 | }{ 241 | {"Empty Bucket Name", fields{"DeleteBucket Empty Bucket Name"}, args{context.Background(), &cosispec.DriverDeleteBucketRequest{BucketId: ""}}, nil, true}, 242 | {"Delete Bucket success", fields{"DeleteBucket Success"}, args{context.Background(), &cosispec.DriverDeleteBucketRequest{BucketId: "test-bucket"}}, &cosispec.DriverDeleteBucketResponse{}, false}, 243 | {"Delete Bucket failure", fields{"DeleteBucket Failure"}, args{context.Background(), &cosispec.DriverDeleteBucketRequest{BucketId: "failed-bucket"}}, nil, true}, 244 | {"Bucket does not exist", fields{"DeleteBucket Does not exist"}, args{context.Background(), &cosispec.DriverDeleteBucketRequest{BucketId: "test-bucket-does-not-exist"}}, nil, true}, 245 | {"Bucket not empty", fields{"DeleteBucket Not Empty"}, args{context.Background(), &cosispec.DriverDeleteBucketRequest{BucketId: "test-bucket-not-empty"}}, nil, true}, 246 | } 247 | 248 | for _, tt := range tests { 249 | t.Run(tt.name, func(t *testing.T) { 250 | b := v1alpha1.Bucket{ 251 | ObjectMeta: metav1.ObjectMeta{ 252 | Name: tt.args.req.GetBucketId(), 253 | }, 254 | Spec: v1alpha1.BucketSpec{ 255 | DriverName: tt.fields.provisioner, 256 | Parameters: createParameters(), 257 | }, 258 | } 259 | bucketClient := fakebucketclientset.NewSimpleClientset(&b) 260 | s := &provisionerServer{ 261 | Provisioner: tt.fields.provisioner, 262 | BucketClientset: bucketClient, 263 | } 264 | got, err := s.DriverDeleteBucket(tt.args.ctx, tt.args.req) 265 | if (err != nil) != tt.wantErr { 266 | t.Errorf("provisionerServer.DriverDeleteBucket() error = %v, wantErr %v", err, tt.wantErr) 267 | return 268 | } 269 | if !reflect.DeepEqual(got, tt.want) { 270 | t.Errorf("provisionerServer.DriverDeleteBucket() = %v, want %v", got, tt.want) 271 | } 272 | }) 273 | } 274 | } 275 | 276 | func Test_provisonerServer_DriverRevokeBucketAccess(t *testing.T) { 277 | type fields struct { 278 | provisioner string 279 | } 280 | type args struct { 281 | ctx context.Context 282 | req *cosispec.DriverRevokeBucketAccessRequest 283 | } 284 | 285 | initializeClients = func(ctx context.Context, clientset *kubernetes.Clientset, parameters map[string]string) (*s3cli.S3Agent, *rgwadmin.API, error) { 286 | _, _, err := fetchSecretNameAndNamespace(parameters) 287 | if err != nil { 288 | t.Fatalf("failed to fetch secret name and namespace: %v", err) 289 | } 290 | s3Client := &s3cli.S3Agent{ 291 | Client: mockS3Client{}, 292 | } 293 | mockClient := &MockClient{ 294 | MockDo: func(req *http.Request) (*http.Response, error) { 295 | if req.Method == http.MethodDelete { 296 | if req.URL.RawQuery == "format=json&uid=test-user" { 297 | return &http.Response{ 298 | StatusCode: 200, 299 | Body: io.NopCloser(bytes.NewReader([]byte(`[]`))), 300 | }, nil 301 | } 302 | } 303 | return nil, fmt.Errorf("unexpected request: %q. method %q. path %q", req.URL.RawQuery, req.Method, req.URL.Path) 304 | }, 305 | } 306 | 307 | rgwAdminClient, err := rgwadmin.New("rgw-my-store:8000", "accesskey", "secretkey", mockClient) 308 | if err != nil { 309 | t.Fatalf("failed to create rgw admin client: %v", err) 310 | } 311 | return s3Client, rgwAdminClient, nil 312 | } 313 | 314 | tests := []struct { 315 | name string 316 | fields fields 317 | args args 318 | want *cosispec.DriverRevokeBucketAccessResponse 319 | wantErr bool 320 | }{ 321 | {"Empty User Name", fields{"RevokeBucketAccess Empty User Name"}, args{context.Background(), &cosispec.DriverRevokeBucketAccessRequest{BucketId: "test-bucket", AccountId: ""}}, nil, true}, 322 | {"Revoke Bucket Access success", fields{"RevokeBucketAccess Success"}, args{context.Background(), &cosispec.DriverRevokeBucketAccessRequest{BucketId: "test-bucket", AccountId: "test-user"}}, &cosispec.DriverRevokeBucketAccessResponse{}, false}, 323 | {"Revoke Bucket Access failure", fields{"RevokeBucketAccess Failure"}, args{context.Background(), &cosispec.DriverRevokeBucketAccessRequest{BucketId: "failed-bucket", AccountId: "failed-user"}}, nil, true}, 324 | } 325 | 326 | for _, tt := range tests { 327 | t.Run(tt.name, func(t *testing.T) { 328 | b := v1alpha1.Bucket{ 329 | ObjectMeta: metav1.ObjectMeta{ 330 | Name: tt.args.req.GetBucketId(), 331 | }, 332 | Spec: v1alpha1.BucketSpec{ 333 | DriverName: tt.fields.provisioner, 334 | Parameters: createParameters(), 335 | }, 336 | } 337 | bucketClient := fakebucketclientset.NewSimpleClientset(&b) 338 | s := &provisionerServer{ 339 | Provisioner: tt.fields.provisioner, 340 | BucketClientset: bucketClient, 341 | } 342 | got, err := s.DriverRevokeBucketAccess(tt.args.ctx, tt.args.req) 343 | if (err != nil) != tt.wantErr { 344 | t.Errorf("provisionerServer.DriverRevokeBucketAccess() error = %v, wantErr %v", err, tt.wantErr) 345 | return 346 | } 347 | if !reflect.DeepEqual(got, tt.want) { 348 | t.Errorf("provisionerServer.DriverRevokeBucketAccess() = %v, want %v", got, tt.want) 349 | } 350 | }) 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.51.12 h1:DvuhIHZXwnjaR1/Gu19gUe1EGPw4J0qSJw4Qs/5PA8g= 2 | github.com/aws/aws-sdk-go v1.51.12/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= 3 | github.com/ceph/go-ceph v0.27.0 h1:5rUTIun/EtUFTH2qb6UokCyw9zul1Vr8iKgJo/VBYr8= 4 | github.com/ceph/go-ceph v0.27.0/go.mod h1:GFlSfPG6JNhliRTZtI4oWbu1QGUMFner9bba1ecNAnk= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= 10 | github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 11 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 12 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 13 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 14 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 15 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 16 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 17 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 18 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 19 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 20 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 21 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 22 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 23 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= 24 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 25 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 26 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 27 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 28 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 29 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 30 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 31 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 32 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 33 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 34 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 35 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 36 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 37 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 38 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 39 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 40 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 41 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 42 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 43 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 44 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 45 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 46 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 47 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 48 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 49 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 50 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 51 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 52 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 53 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 54 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 55 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 56 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 57 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 58 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 60 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 61 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 62 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 63 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 64 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 65 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 66 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 67 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 68 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 69 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 70 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 71 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 72 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 73 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 74 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 75 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 76 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 77 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 78 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 79 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 80 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 81 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 82 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 83 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 84 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 85 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 86 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 87 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 88 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 89 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 90 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 91 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 92 | go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= 93 | go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= 94 | go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= 95 | go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= 96 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 97 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 100 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 101 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 102 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 103 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 104 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 105 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 106 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 107 | golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= 108 | golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= 109 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 110 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 111 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 112 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 113 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 115 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 118 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 119 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 120 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 121 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 122 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 123 | golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 124 | golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 125 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 126 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 127 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 128 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 129 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 130 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 131 | golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= 132 | golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= 133 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 134 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 135 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 136 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 137 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 138 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 139 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= 140 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 141 | google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= 142 | google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= 143 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 144 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 145 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 146 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 147 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 148 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 149 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 150 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 151 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 152 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 153 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 154 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 155 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 156 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 157 | k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= 158 | k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= 159 | k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= 160 | k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 161 | k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= 162 | k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= 163 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 164 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 165 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 166 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 167 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 168 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 169 | sigs.k8s.io/container-object-storage-interface v0.0.0-20250915185608-01dcd1a8c124 h1:VAD1l39SaGJAQkE8VnemfdJOAAL4gLqagp5FNzbYlEI= 170 | sigs.k8s.io/container-object-storage-interface v0.0.0-20250915185608-01dcd1a8c124/go.mod h1:p2USZm0jozsoTnErjq5NukmnGr6py9e5GLOGO8iIsp8= 171 | sigs.k8s.io/container-object-storage-interface/client v0.0.0-20250915175017-b1ac3c818b6e h1:0TxSmx6lZnkDmD2p4c03mfSSKyT8tg0d5UJyKa1YVwk= 172 | sigs.k8s.io/container-object-storage-interface/client v0.0.0-20250915175017-b1ac3c818b6e/go.mod h1:RQSwGVkJ9vBo02N1tTWmqHqykln8K2d/dHgdXWie2I4= 173 | sigs.k8s.io/container-object-storage-interface/proto v0.0.0-20250728140943-f18af7ae56c9 h1:ZgwFlo+QpQyvuA1TVJ3dPxoB1pRBOlROOmtIhYS7yxY= 174 | sigs.k8s.io/container-object-storage-interface/proto v0.0.0-20250728140943-f18af7ae56c9/go.mod h1:MmjK06anCgKf/ESX/sqx+G9DV8i19PJiLSutp0TNF5g= 175 | sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw= 176 | sigs.k8s.io/controller-runtime v0.18.4/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg= 177 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 178 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 179 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 180 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 181 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 182 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 183 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 184 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 185 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 186 | --------------------------------------------------------------------------------