├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── backport.yml │ ├── chainsaw-e2e-test-1.28.yaml │ ├── chainsaw-e2e-test-1.29.yaml │ ├── chainsaw-e2e-test-1.30.yaml │ ├── chainsaw-e2e-test-1.31.yaml │ ├── ci.yml │ ├── codeql.yml │ ├── commands.yml │ ├── promote.yml │ ├── publish.yml │ ├── release-candidate-publish-and-pr.yml │ ├── tag-rc.yml │ └── tag.yml ├── .gitignore ├── .gitmodules ├── .golangci.yml ├── .husky ├── hooks │ ├── pre-commit │ └── pre-push └── husky.mk ├── .mirrord ├── README.MD ├── mirrord.json └── mirrord.mk ├── CODE_OF_CONDUCT.md ├── DCO ├── LICENSE ├── Makefile ├── OWNERS.md ├── PROVIDER_CHECKLIST.md ├── README.md ├── apis ├── ceph.go ├── generate.go ├── provider-ceph │ ├── provider-ceph.go │ └── v1alpha1 │ │ ├── acl_types.go │ │ ├── bucket_types.go │ │ ├── condition.go │ │ ├── doc.go │ │ ├── groupversion_info.go │ │ ├── lifecycleconfiguration_types.go │ │ ├── objectlockconfiguration_types.go │ │ ├── utils.go │ │ ├── versioningconfiguration_types.go │ │ ├── zz_generated.deepcopy.go │ │ ├── zz_generated.managed.go │ │ └── zz_generated.managedlist.go └── v1alpha1 │ ├── doc.go │ ├── groupversion_info.go │ ├── providerconfig_types.go │ ├── providerconfigusage_types.go │ ├── storeconfig_types.go │ ├── zz_generated.deepcopy.go │ ├── zz_generated.pc.go │ ├── zz_generated.pcu.go │ └── zz_generated.pculist.go ├── cluster ├── Dockerfile ├── images │ └── provider-ceph │ │ ├── Dockerfile │ │ └── Makefile └── local │ └── integration_tests.sh ├── cmd └── provider │ └── main.go ├── docs ├── AUTHENTICATION.md ├── AUTOPAUSE.md ├── DEVELOPMENT.md ├── RELEASE_PROCESS.md ├── TESTING.md ├── WEBHOOKS.md └── debug │ ├── DEBUG_LOGS.md │ ├── deployment-runtime-config.yaml │ └── provider.yaml ├── e2e ├── kind │ ├── kind-config-1.28.yaml │ ├── kind-config-1.29.yaml │ ├── kind-config-1.30.yaml │ └── kind-config-1.31.yaml ├── localstack │ ├── README.md │ ├── localstack-deployment.yaml │ ├── localstack-provider-cfg-host.yaml │ └── localstack-provider-cfg.yaml └── tests │ ├── ceph │ ├── .chainsaw.yaml │ └── chainsaw-test.yaml │ └── stable │ ├── .chainsaw.yaml │ └── chainsaw-test.yaml ├── examples ├── provider │ ├── config.yaml │ └── provider.yaml ├── sample │ └── bucket.yaml └── storeconfig │ └── vault.yaml ├── go.mod ├── go.sum ├── hack ├── boilerplate.go.txt ├── deploy-provider.sh ├── expect_bucket.sh ├── generate-tests.sh ├── helpers │ ├── addtype.sh │ ├── apis │ │ └── GROUP_LOWER │ │ │ ├── APIVERSION │ │ │ ├── KIND_LOWER_types.go.tmpl │ │ │ ├── doc.go.tmpl │ │ │ └── groupversion_info.go.tmpl │ │ │ └── GROUP_LOWER.go.tmpl │ ├── controller │ │ └── KIND_LOWER │ │ │ ├── KIND_LOWER.go.tmpl │ │ │ └── KIND_LOWER_test.go.tmpl │ └── prepare.sh ├── install-pc-ceph-cluster.sh ├── load-images.sh ├── localtunnel.yaml ├── update-image-tag.sh └── update-kind-nodes.sh ├── internal ├── backendstore │ ├── backend.go │ ├── backendstore.go │ └── backendstorefakes │ │ ├── fake_s3client.go │ │ └── fake_stsclient.go ├── consts │ └── consts.go ├── controller │ ├── bucket │ │ ├── acl.go │ │ ├── acl_test.go │ │ ├── bucket_backends.go │ │ ├── bucket_validation_webhook.go │ │ ├── connector.go │ │ ├── consts.go │ │ ├── create.go │ │ ├── create_test.go │ │ ├── delete.go │ │ ├── delete_test.go │ │ ├── disconnect.go │ │ ├── helpers.go │ │ ├── helpers_test.go │ │ ├── lifecycleconfiguration.go │ │ ├── lifecycleconfiguration_test.go │ │ ├── objectlockconfiguration.go │ │ ├── objectlockconfiguration_test.go │ │ ├── observe.go │ │ ├── observe_test.go │ │ ├── policy.go │ │ ├── policy_test.go │ │ ├── setup.go │ │ ├── subresources.go │ │ ├── update.go │ │ ├── update_test.go │ │ ├── versioningconfiguration.go │ │ └── versioningconfiguration_test.go │ ├── ceph.go │ ├── doc.go │ ├── providerconfig │ │ ├── backendmonitor │ │ │ ├── backendmonitor.go │ │ │ └── backendmonitor_controller.go │ │ ├── healthcheck │ │ │ ├── healthcheck.go │ │ │ ├── healthcheck_controller.go │ │ │ ├── healthcheck_controller_test.go │ │ │ ├── helpers.go │ │ │ └── helpers_test.go │ │ └── setup.go │ └── s3clienthandler │ │ ├── role_session_name.go │ │ ├── role_session_name_test.go │ │ ├── s3clienthandler.go │ │ └── s3clienthandler_test.go ├── features │ └── features.go ├── otel │ ├── otel.go │ └── traces │ │ ├── traces.go │ │ └── traces_test.go ├── rgw │ ├── acl.go │ ├── acl_helpers.go │ ├── acl_test.go │ ├── assumerole.go │ ├── bucket.go │ ├── bucket_helpers.go │ ├── bucket_helpers_test.go │ ├── bucket_test.go │ ├── client.go │ ├── lifecycleconfig.go │ ├── lifecycleconfig_helpers.go │ ├── lifecycleconfig_test.go │ ├── object.go │ ├── objectlockconfiguration.go │ ├── objectlockconfiguration_helpers.go │ ├── policy.go │ ├── versioningconfiguration.go │ └── versioningconfiguration_helpers.go └── utils │ ├── randomstring │ ├── randomstring.go │ ├── randomstring_test.go │ └── randomstringfakes │ │ └── fake_generator.go │ ├── utils.go │ └── utils_test.go ├── package ├── crds │ ├── ceph.crossplane.io_providerconfigs.yaml │ ├── ceph.crossplane.io_providerconfigusages.yaml │ ├── ceph.crossplane.io_storeconfigs.yaml │ └── provider-ceph.ceph.crossplane.io_buckets.yaml ├── crossplane.yaml └── webhookconfigurations │ └── manifests.yaml └── staging └── validatingwebhookconfiguration ├── .gitignore ├── kustomization.yaml ├── object-selector-patch.yaml ├── service-patch-cert-manager.yaml ├── service-patch-dev.tpl.yaml ├── service-patch-stock.yaml └── service-patch.yaml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Help us diagnose and fix bugs in Crossplane 4 | labels: bug 5 | --- 6 | 13 | 14 | ### What happened? 15 | 19 | 20 | 21 | ### How can we reproduce it? 22 | 27 | 28 | ### What environment did it happen in? 29 | Crossplane version: 30 | 31 | 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Help us make Provider Ceph more useful 4 | labels: enhancement 5 | --- 6 | 13 | 14 | ### What problem are you facing? 15 | 20 | 21 | ### How could Provider Ceph help solve your problem? 22 | 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Description of your changes 10 | 11 | 15 | 16 | I have: 17 | 18 | - [ ] Run `make reviewable` to ensure this PR is ready for review. 19 | - [ ] Run `make ceph-chainsaw` to validate these changes against Ceph. This step is not always necessary. However, for changes related to S3 calls it is sensible to validate against an actual Ceph cluster. Localstack is used in our CI Chainsaw suite for convenience and there can be disparity in S3 behaviours betwee it and Ceph. See `docs/TESTING.md` for information on how to run tests against a Ceph cluster. 20 | - [ ] Added `backport release-x.y` labels to auto-backport this PR if necessary. 21 | 22 | ### How has this code been tested 23 | 24 | 29 | 30 | [contribution process]: https://git.io/fj2m9 31 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport 2 | 3 | on: 4 | # NOTE(negz): This is a risky target, but we run this action only when and if 5 | # a PR is closed, then filter down to specifically merged PRs. We also don't 6 | # invoke any scripts, etc from within the repo. I believe the fact that we'll 7 | # be able to review PRs before this runs makes this fairly safe. 8 | # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 9 | pull_request_target: 10 | types: [closed] 11 | # See also commands.yml for the /backport triggered variant of this workflow. 12 | 13 | jobs: 14 | # NOTE(negz): I tested many backport GitHub actions before landing on this 15 | # one. Many do not support merge commits, or do not support pull requests with 16 | # more than one commit. This one does. It also handily links backport PRs with 17 | # new PRs, and provides commentary and instructions when it can't backport. 18 | # The main gotcha with this action is that PRs _must_ be labelled before they're 19 | # merged to trigger a backport. 20 | open-pr: 21 | runs-on: ubuntu-22.04 22 | if: github.event.pull_request.merged 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v3 26 | 27 | - name: Open Backport PR 28 | uses: korthout/backport-action@v1 29 | -------------------------------------------------------------------------------- /.github/workflows/chainsaw-e2e-test-1.28.yaml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by hack/generate-tests.sh 2 | name: chainsaw e2e test 1.28 3 | on: [push] 4 | concurrency: 5 | group: chainsaw-1.28-${{ github.ref }}-1 6 | cancel-in-progress: true 7 | permissions: 8 | contents: read 9 | jobs: 10 | test: 11 | name: chainsaw e2e test 1.28 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Cancel Previous Runs 15 | uses: styfle/cancel-workflow-action@0.9.1 16 | with: 17 | access_token: ${{ github.token }} 18 | 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | submodules: true 23 | 24 | - name: Setup Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: '1.23' 28 | 29 | - name: Vendor Dependencies 30 | run: make vendor vendor.check 31 | 32 | - name: Docker cache 33 | uses: ScribeMD/docker-cache@0.3.7 34 | with: 35 | key: docker-${{ runner.os }}-${{ hashFiles('go.sum') }}} 36 | 37 | - name: Run chainsaw tests 1.28 38 | run: make chainsaw 39 | env: 40 | LATEST_KUBE_VERSION: '1.28' 41 | AWS_ACCESS_KEY_ID: 'Dummy' 42 | AWS_SECRET_ACCESS_KEY: 'Dummy' 43 | AWS_DEFAULT_REGION: 'us-east-1' 44 | -------------------------------------------------------------------------------- /.github/workflows/chainsaw-e2e-test-1.29.yaml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by hack/generate-tests.sh 2 | name: chainsaw e2e test 1.29 3 | on: [push] 4 | concurrency: 5 | group: chainsaw-1.29-${{ github.ref }}-1 6 | cancel-in-progress: true 7 | permissions: 8 | contents: read 9 | jobs: 10 | test: 11 | name: chainsaw e2e test 1.29 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Cancel Previous Runs 15 | uses: styfle/cancel-workflow-action@0.9.1 16 | with: 17 | access_token: ${{ github.token }} 18 | 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | submodules: true 23 | 24 | - name: Setup Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: '1.23' 28 | 29 | - name: Vendor Dependencies 30 | run: make vendor vendor.check 31 | 32 | - name: Docker cache 33 | uses: ScribeMD/docker-cache@0.3.7 34 | with: 35 | key: docker-${{ runner.os }}-${{ hashFiles('go.sum') }}} 36 | 37 | - name: Run chainsaw tests 1.29 38 | run: make chainsaw 39 | env: 40 | LATEST_KUBE_VERSION: '1.29' 41 | AWS_ACCESS_KEY_ID: 'Dummy' 42 | AWS_SECRET_ACCESS_KEY: 'Dummy' 43 | AWS_DEFAULT_REGION: 'us-east-1' 44 | -------------------------------------------------------------------------------- /.github/workflows/chainsaw-e2e-test-1.30.yaml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by hack/generate-tests.sh 2 | name: chainsaw e2e test 1.30 3 | on: [push] 4 | concurrency: 5 | group: chainsaw-1.30-${{ github.ref }}-1 6 | cancel-in-progress: true 7 | permissions: 8 | contents: read 9 | jobs: 10 | test: 11 | name: chainsaw e2e test 1.30 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Cancel Previous Runs 15 | uses: styfle/cancel-workflow-action@0.9.1 16 | with: 17 | access_token: ${{ github.token }} 18 | 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | submodules: true 23 | 24 | - name: Setup Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: '1.23' 28 | 29 | - name: Vendor Dependencies 30 | run: make vendor vendor.check 31 | 32 | - name: Docker cache 33 | uses: ScribeMD/docker-cache@0.3.7 34 | with: 35 | key: docker-${{ runner.os }}-${{ hashFiles('go.sum') }}} 36 | 37 | - name: Run chainsaw tests 1.30 38 | run: make chainsaw 39 | env: 40 | LATEST_KUBE_VERSION: '1.30' 41 | AWS_ACCESS_KEY_ID: 'Dummy' 42 | AWS_SECRET_ACCESS_KEY: 'Dummy' 43 | AWS_DEFAULT_REGION: 'us-east-1' 44 | -------------------------------------------------------------------------------- /.github/workflows/chainsaw-e2e-test-1.31.yaml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by hack/generate-tests.sh 2 | name: chainsaw e2e test 1.31 3 | on: [push] 4 | concurrency: 5 | group: chainsaw-1.31-${{ github.ref }}-1 6 | cancel-in-progress: true 7 | permissions: 8 | contents: read 9 | jobs: 10 | test: 11 | name: chainsaw e2e test 1.31 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Cancel Previous Runs 15 | uses: styfle/cancel-workflow-action@0.9.1 16 | with: 17 | access_token: ${{ github.token }} 18 | 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | submodules: true 23 | 24 | - name: Setup Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: '1.23' 28 | 29 | - name: Vendor Dependencies 30 | run: make vendor vendor.check 31 | 32 | - name: Docker cache 33 | uses: ScribeMD/docker-cache@0.3.7 34 | with: 35 | key: docker-${{ runner.os }}-${{ hashFiles('go.sum') }}} 36 | 37 | - name: Run chainsaw tests 1.31 38 | run: make chainsaw 39 | env: 40 | LATEST_KUBE_VERSION: '1.31' 41 | AWS_ACCESS_KEY_ID: 'Dummy' 42 | AWS_SECRET_ACCESS_KEY: 'Dummy' 43 | AWS_DEFAULT_REGION: 'us-east-1' 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release-* 8 | pull_request: {} 9 | workflow_dispatch: {} 10 | 11 | concurrency: 12 | group: ci-${{ github.ref }}-1 13 | cancel-in-progress: true 14 | 15 | permissions: 16 | contents: read 17 | 18 | env: 19 | # Common versions 20 | GO_VERSION: '1.23' 21 | DOCKER_BUILDX_VERSION: 'v0.9.1' 22 | 23 | # Common users. We can't run a step 'if secrets.XXX != ""' but we can run a 24 | # step 'if env.XXX' != ""', so we copy these to succinctly test whether 25 | # credentials have been provided before trying to run steps that need them. 26 | UPBOUND_MARKETPLACE_PUSH_ROBOT_USR: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR }} 27 | jobs: 28 | detect-noop: 29 | runs-on: ubuntu-22.04 30 | outputs: 31 | noop: ${{ steps.noop.outputs.should_skip }} 32 | steps: 33 | - name: Detect No-op Changes 34 | id: noop 35 | uses: fkirc/skip-duplicate-actions@v5.3.1 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | paths_ignore: '["**.md", "**.png", "**.jpg"]' 39 | do_not_skip: '["workflow_dispatch", "schedule", "push"]' 40 | 41 | 42 | lint: 43 | runs-on: ubuntu-22.04 44 | needs: detect-noop 45 | if: needs.detect-noop.outputs.noop != 'true' 46 | 47 | steps: 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | with: 51 | submodules: true 52 | 53 | - name: Setup Go 54 | uses: actions/setup-go@v5 55 | with: 56 | go-version: ${{ env.GO_VERSION }} 57 | 58 | - name: Vendor Dependencies 59 | run: make vendor vendor.check 60 | 61 | - name: Lint 62 | run: make lint 63 | 64 | nilcheck: 65 | runs-on: ubuntu-22.04 66 | needs: detect-noop 67 | if: needs.detect-noop.outputs.noop != 'true' 68 | 69 | steps: 70 | - name: Checkout 71 | uses: actions/checkout@v4 72 | with: 73 | submodules: true 74 | 75 | - name: Setup Go 76 | uses: actions/setup-go@v5 77 | with: 78 | go-version: ${{ env.GO_VERSION }} 79 | 80 | - name: Vendor Dependencies 81 | run: make vendor vendor.check 82 | 83 | - name: Nilcheck 84 | run: make nilcheck 85 | 86 | vulncheck: 87 | runs-on: ubuntu-22.04 88 | needs: detect-noop 89 | if: needs.detect-noop.outputs.noop != 'true' 90 | 91 | steps: 92 | - name: Checkout 93 | uses: actions/checkout@v4 94 | with: 95 | submodules: true 96 | 97 | - name: Setup Go 98 | uses: actions/setup-go@v5 99 | with: 100 | go-version: ${{ env.GO_VERSION }} 101 | 102 | - name: Vendor Dependencies 103 | run: make vendor vendor.check 104 | 105 | - name: Vulncheck 106 | run: make vulncheck 107 | 108 | check-diff: 109 | runs-on: ubuntu-22.04 110 | needs: detect-noop 111 | if: needs.detect-noop.outputs.noop != 'true' 112 | 113 | steps: 114 | - name: Checkout 115 | uses: actions/checkout@v4 116 | with: 117 | submodules: true 118 | 119 | - name: Setup Go 120 | uses: actions/setup-go@v5 121 | with: 122 | go-version: ${{ env.GO_VERSION }} 123 | 124 | - name: Vendor Dependencies 125 | run: make vendor vendor.check 126 | 127 | - name: Check Diff 128 | id: check-diff 129 | run: | 130 | mkdir _output 131 | make check-diff-pkg 132 | 133 | - name: Show diff 134 | if: failure() && steps.check-diff.outcome == 'failure' 135 | run: git diff 136 | 137 | unit-tests: 138 | runs-on: ubuntu-22.04 139 | needs: detect-noop 140 | if: needs.detect-noop.outputs.noop != 'true' 141 | 142 | steps: 143 | - name: Checkout 144 | uses: actions/checkout@v4 145 | with: 146 | submodules: true 147 | 148 | - name: Fetch History 149 | run: git fetch --prune --unshallow 150 | 151 | - name: Setup Go 152 | uses: actions/setup-go@v5 153 | with: 154 | go-version: ${{ env.GO_VERSION }} 155 | 156 | - name: Vendor Dependencies 157 | run: make vendor vendor.check 158 | 159 | - name: Run Unit Tests 160 | run: make -j2 test 161 | 162 | - name: Publish Unit Test Coverage 163 | uses: codecov/codecov-action@v3 164 | with: 165 | flags: unittests 166 | file: _output/tests/linux_amd64/coverage.txt 167 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release-* 8 | - fix-codeql-* 9 | workflow_dispatch: {} 10 | 11 | concurrency: 12 | group: codeql-${{ github.ref }}-1 13 | cancel-in-progress: true 14 | 15 | permissions: 16 | actions: read 17 | contents: read 18 | 19 | jobs: 20 | detect-noop: 21 | runs-on: ubuntu-22.04 22 | outputs: 23 | noop: ${{ steps.noop.outputs.should_skip }} 24 | steps: 25 | - name: Detect No-op Changes 26 | id: noop 27 | uses: fkirc/skip-duplicate-actions@v5.2.0 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | paths_ignore: '["**.md", "**.png", "**.jpg"]' 31 | do_not_skip: '["workflow_dispatch", "schedule", "push"]' 32 | concurrent_skipping: false 33 | 34 | analyze: 35 | runs-on: ubuntu-22.04 36 | permissions: 37 | security-events: write 38 | needs: detect-noop 39 | if: needs.detect-noop.outputs.noop != 'true' 40 | 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v4 44 | with: 45 | submodules: true 46 | 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v2 49 | 50 | # Custom Go version because of: https://github.com/github/codeql/issues/13992#issuecomment-1711721716 51 | - uses: actions/setup-go@v5 52 | with: 53 | go-version: '1.21' 54 | 55 | - name: Perform CodeQL Analysis 56 | uses: github/codeql-action/analyze@v2 57 | -------------------------------------------------------------------------------- /.github/workflows/commands.yml: -------------------------------------------------------------------------------- 1 | name: Comment Commands 2 | 3 | on: issue_comment 4 | 5 | jobs: 6 | points: 7 | runs-on: ubuntu-22.04 8 | if: startsWith(github.event.comment.body, '/points') 9 | 10 | steps: 11 | - name: Extract Command 12 | id: command 13 | uses: xt0rted/slash-command-action@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | command: points 17 | reaction: "true" 18 | reaction-type: "eyes" 19 | allow-edits: "false" 20 | permission-level: write 21 | - name: Handle Command 22 | uses: actions/github-script@v4 23 | env: 24 | POINTS: ${{ steps.command.outputs.command-arguments }} 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | script: | 28 | const points = process.env.POINTS 29 | 30 | if (isNaN(parseInt(points))) { 31 | console.log("Malformed command - expected '/points '") 32 | github.reactions.createForIssueComment({ 33 | owner: context.repo.owner, 34 | repo: context.repo.repo, 35 | comment_id: context.payload.comment.id, 36 | content: "confused" 37 | }) 38 | return 39 | } 40 | const label = "points/" + points 41 | 42 | // Delete our needs-points-label label. 43 | try { 44 | await github.issues.deleteLabel({ 45 | issue_number: context.issue.number, 46 | owner: context.repo.owner, 47 | repo: context.repo.repo, 48 | name: ['needs-points-label'] 49 | }) 50 | console.log("Deleted 'needs-points-label' label.") 51 | } 52 | catch(e) { 53 | console.log("Label 'needs-points-label' probably didn't exist.") 54 | } 55 | 56 | // Add our points label. 57 | github.issues.addLabels({ 58 | issue_number: context.issue.number, 59 | owner: context.repo.owner, 60 | repo: context.repo.repo, 61 | labels: [label] 62 | }) 63 | console.log("Added '" + label + "' label.") 64 | 65 | # NOTE(negz): See also backport.yml, which is the variant that triggers on PR 66 | # merge rather than on comment. 67 | backport: 68 | runs-on: ubuntu-22.04 69 | if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/backport') 70 | steps: 71 | - name: Extract Command 72 | id: command 73 | uses: xt0rted/slash-command-action@v1 74 | with: 75 | repo-token: ${{ secrets.GITHUB_TOKEN }} 76 | command: backport 77 | reaction: "true" 78 | reaction-type: "eyes" 79 | allow-edits: "false" 80 | permission-level: write 81 | 82 | - name: Checkout 83 | uses: actions/checkout@v3 84 | 85 | - name: Open Backport PR 86 | uses: korthout/backport-action@v1 87 | -------------------------------------------------------------------------------- /.github/workflows/promote.yml: -------------------------------------------------------------------------------- 1 | name: Promote 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Release version (e.g. v0.1.0)' 8 | required: true 9 | channel: 10 | description: 'Release channel' 11 | required: true 12 | default: 'stable' 13 | 14 | env: 15 | # Common users. We can't run a step 'if secrets.AWS_USR != ""' but we can run 16 | # a step 'if env.AWS_USR' != ""', so we copy these to succinctly test whether 17 | # credentials have been provided before trying to run steps that need them. 18 | DOCKER_USR: ${{ secrets.DOCKER_USR }} 19 | AWS_USR: ${{ secrets.AWS_USR }} 20 | 21 | jobs: 22 | promote-artifacts: 23 | runs-on: ubuntu-22.04 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | with: 29 | submodules: true 30 | 31 | - name: Fetch History 32 | run: git fetch --prune --unshallow 33 | 34 | - name: Login to Docker 35 | uses: docker/login-action@v1 36 | if: env.DOCKER_USR != '' 37 | with: 38 | username: ${{ secrets.DOCKER_USR }} 39 | password: ${{ secrets.DOCKER_PSW }} 40 | 41 | - name: Promote Artifacts in S3 and Docker Hub 42 | if: env.AWS_USR != '' && env.DOCKER_USR != '' 43 | run: make -j2 promote BRANCH_NAME=${GITHUB_REF##*/} 44 | env: 45 | VERSION: ${{ github.event.inputs.version }} 46 | CHANNEL: ${{ github.event.inputs.channel }} 47 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_USR }} 48 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_PSW }} 49 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: {} 5 | 6 | concurrency: 7 | group: ci-${{ github.ref }}-1 8 | cancel-in-progress: true 9 | 10 | permissions: 11 | contents: read 12 | 13 | env: 14 | # Common versions 15 | GO_VERSION: '1.23' 16 | DOCKER_BUILDX_VERSION: 'v0.9.1' 17 | 18 | # Common users. We can't run a step 'if secrets.XXX != ""' but we can run a 19 | # step 'if env.XXX' != ""', so we copy these to succinctly test whether 20 | # credentials have been provided before trying to run steps that need them. 21 | UPBOUND_MARKETPLACE_PUSH_ROBOT_USR: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR }} 22 | jobs: 23 | 24 | publish-artifacts: 25 | runs-on: ubuntu-22.04 26 | 27 | steps: 28 | - name: Setup QEMU 29 | uses: docker/setup-qemu-action@v3 30 | with: 31 | platforms: all 32 | 33 | - name: Setup Docker Buildx 34 | uses: docker/setup-buildx-action@v3 35 | with: 36 | version: ${{ env.DOCKER_BUILDX_VERSION }} 37 | install: true 38 | 39 | - name: Login to Upbound 40 | uses: docker/login-action@v3 41 | if: env.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR != '' 42 | with: 43 | registry: xpkg.upbound.io 44 | username: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR }} 45 | password: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_PSW }} 46 | 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | with: 50 | submodules: true 51 | 52 | - name: Fetch History 53 | run: git fetch --prune --unshallow 54 | 55 | - name: Setup Go 56 | uses: actions/setup-go@v5 57 | with: 58 | go-version: ${{ env.GO_VERSION }} 59 | 60 | - name: Vendor Dependencies 61 | run: make vendor vendor.check 62 | 63 | - name: Build Artifacts 64 | run: make -j2 build.all 65 | env: 66 | # We're using docker buildx, which doesn't actually load the images it 67 | # builds by default. Specifying --load does so. 68 | BUILD_ARGS: "--load" 69 | 70 | - name: Publish Artifacts to GitHub 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: output 74 | path: _output/** 75 | 76 | - name: Publish Artifacts 77 | if: env.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR != '' 78 | run: make publish BRANCH_NAME=${GITHUB_REF##*/} 79 | -------------------------------------------------------------------------------- /.github/workflows/release-candidate-publish-and-pr.yml: -------------------------------------------------------------------------------- 1 | name: Release Candidate Publish and PR 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: tag-${{ github.ref }}-1 10 | cancel-in-progress: true 11 | 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | 16 | env: 17 | # Common versions 18 | GO_VERSION: '1.23' 19 | DOCKER_BUILDX_VERSION: 'v0.9.1' 20 | 21 | # Common users. We can't run a step 'if secrets.XXX != ""' but we can run a 22 | # step 'if env.XXX' != ""', so we copy these to succinctly test whether 23 | # credentials have been provided before trying to run steps that need them. 24 | UPBOUND_MARKETPLACE_PUSH_ROBOT_USR: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR }} 25 | 26 | jobs: 27 | # Upon a merge to main, this job checks out main and updates package/crossplane.yaml 28 | # and README.md to point to the latest version (latest git commit SHA). This version 29 | # is then used to create and publish a new package to the Upbound marketplace. Finally, 30 | # using our updated branch an automated PR is opened against main with the version changes. 31 | release-candidate-publish-and-pr: 32 | # We don't want to run this on main upon merging the automated PR we are creating, 33 | # otherwise we would end up in an endless loop of automated PRs. 34 | if: > 35 | github.event_name == 'push' && 36 | github.ref == 'refs/heads/main' && 37 | !contains(github.event.head_commit.message, 'Set release candidate version') 38 | runs-on: ubuntu-22.04 39 | permissions: 40 | contents: write 41 | pull-requests: write 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | with: 46 | fetch-depth: 0 47 | 48 | - name: Get Version 49 | run: | 50 | make submodules 51 | version=$(make common.buildvars | grep "^VERSION=" | cut -d '=' -f 2) 52 | echo "VERSION=$version" >> $GITHUB_ENV 53 | branch=candidate-$version 54 | echo "BRANCH=$branch" >> $GITHUB_ENV 55 | 56 | - name: Set git config 57 | run: | 58 | git config user.name "GitHub Actions" 59 | git config user.email "<>" 60 | 61 | - name: Create a new branch 62 | run: | 63 | git checkout -b $BRANCH 64 | 65 | - name: Update image tag 66 | run: /bin/bash ./hack/update-image-tag.sh $VERSION 67 | 68 | - name: Commit 69 | run: | 70 | git add . 71 | git commit -m "Set release candidate version [skip ci]: ${VERSION}" 72 | git push --set-upstream origin $BRANCH 73 | 74 | - name: Setup QEMU 75 | uses: docker/setup-qemu-action@v3 76 | with: 77 | platforms: all 78 | 79 | - name: Setup Docker Buildx 80 | uses: docker/setup-buildx-action@v3 81 | with: 82 | version: ${{ env.DOCKER_BUILDX_VERSION }} 83 | install: true 84 | 85 | - name: Login to Upbound 86 | uses: docker/login-action@v3 87 | if: env.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR != '' 88 | with: 89 | registry: xpkg.upbound.io 90 | username: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR }} 91 | password: ${{ secrets.UPBOUND_MARKETPLACE_PUSH_ROBOT_PSW }} 92 | 93 | - name: Setup Go 94 | uses: actions/setup-go@v5 95 | with: 96 | go-version: ${{ env.GO_VERSION }} 97 | 98 | - name: Vendor Dependencies 99 | run: make vendor vendor.check 100 | 101 | - name: Build Artifacts 102 | run: make -j2 build.all 103 | env: 104 | # We're using docker buildx, which doesn't actually load the images it 105 | # builds by default. Specifying --load does so. 106 | BUILD_ARGS: "--load" 107 | 108 | - name: Publish Artifacts to GitHub 109 | uses: actions/upload-artifact@v4 110 | with: 111 | name: output 112 | path: _output/** 113 | 114 | - name: Publish Artifacts 115 | if: env.UPBOUND_MARKETPLACE_PUSH_ROBOT_USR != '' 116 | run: make publish BRANCH_NAME=${GITHUB_REF##*/} 117 | 118 | - name: Create a PR 119 | run: | 120 | gh pr create \ 121 | --base ${GITHUB_REF#refs/heads/} \ 122 | --head $BRANCH \ 123 | --title "Release Candidate: ${VERSION}" \ 124 | --body "This PR is created by GitHub Actions." 125 | env: 126 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 127 | -------------------------------------------------------------------------------- /.github/workflows/tag-rc.yml: -------------------------------------------------------------------------------- 1 | name: Tag Release Canditate 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Release canditate version (e.g. v0.1.0-rc.0)' 8 | required: true 9 | message: 10 | description: 'Tag message' 11 | required: true 12 | 13 | concurrency: 14 | group: tag-${{ github.ref }}-1 15 | cancel-in-progress: true 16 | 17 | permissions: 18 | contents: write 19 | 20 | jobs: 21 | create-tag: 22 | runs-on: ubuntu-22.04 23 | env: 24 | VERSION: ${{ github.event.inputs.version }} 25 | MESSAGE: ${{ github.event.inputs.message }} 26 | BRANCH: main 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | 31 | - name: Set git config 32 | run: | 33 | git config user.name "GitHub Actions" 34 | git config user.email "<>" 35 | 36 | - name: Tag 37 | run: | 38 | git tag $VERSION -m "$MESSAGE" 39 | git push origin $VERSION 40 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Tag 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Release version (e.g. v0.1.0)' 8 | required: true 9 | message: 10 | description: 'Tag message' 11 | required: true 12 | 13 | concurrency: 14 | group: tag-${{ github.ref }}-1 15 | cancel-in-progress: true 16 | 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | 21 | jobs: 22 | create-tag: 23 | runs-on: ubuntu-22.04 24 | env: 25 | VERSION: ${{ github.event.inputs.version }} 26 | MESSAGE: ${{ github.event.inputs.message }} 27 | BRANCH: "release-${{ github.event.inputs.version }}" 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Set git config 33 | run: | 34 | git config user.name "GitHub Actions" 35 | git config user.email "<>" 36 | 37 | - name: Create a new branch 38 | run: | 39 | git checkout -b $BRANCH 40 | 41 | - name: Update image tag 42 | run: /bin/bash ./hack/update-image-tag.sh $VERSION 43 | 44 | - name: Commit 45 | run: | 46 | git add . 47 | git commit -m "Bump version [skip ci]: ${VERSION}" 48 | git push --set-upstream origin $BRANCH 49 | 50 | - name: Tag 51 | run: | 52 | git tag $VERSION -m "$MESSAGE" 53 | git push origin $VERSION 54 | 55 | - name: Create a PR 56 | run: | 57 | gh pr create \ 58 | --base ${GITHUB_REF#refs/heads/} \ 59 | --head $BRANCH \ 60 | --title "Bump version: ${VERSION}" \ 61 | --body "This PR is created by GitHub Actions." 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /.cache 3 | /.work 4 | /_output 5 | cover.out 6 | /vendor 7 | /.vendor-new 8 | __debug* 9 | .vscode 10 | .idea 11 | .DS_Store 12 | kind-logs* 13 | kubeconfig 14 | package/provider-ceph-*.xpkg 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "build"] 2 | path = build 3 | url = https://github.com/crossplane/build 4 | -------------------------------------------------------------------------------- /.husky/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PROJ_DIR=$(git rev-parse --show-toplevel) 4 | HUSKY_VERSION=$(cat $PROJ_DIR/.husky/husky.mk | grep "HUSKY_VERSION ?=" | awk '{print $3}') 5 | 6 | find $PROJ_DIR/.cache/tools -name husky-$HUSKY_VERSION -exec {} install \; 7 | -------------------------------------------------------------------------------- /.husky/hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ "$SKIP_GIT_PUSH_HOOK" ]]; then exit 0; fi 4 | 5 | set -e 6 | 7 | if git status --short | grep -qv "??"; then 8 | git stash 9 | function unstash() { 10 | git reset --hard 11 | git stash pop 12 | } 13 | trap unstash EXIT 14 | fi 15 | 16 | # Avoid commiting other then stock manifests. 17 | export WEBHOOK_TYPE=stock 18 | 19 | make generate generate-pkg generate-tests 20 | git diff --exit-code --quiet || (git status && exit 1) 21 | 22 | make lint go.test.unit nilcheck vulncheck 23 | -------------------------------------------------------------------------------- /.husky/husky.mk: -------------------------------------------------------------------------------- 1 | HUSKY_VERSION ?= v0.2.16 2 | HUSKY ?= $(TOOLS_HOST_DIR)/husky-$(HUSKY_VERSION) 3 | 4 | husky.install: $(HUSKY) 5 | @$(HUSKY) install 6 | 7 | ## Download husky locally if necessary. 8 | $(HUSKY): 9 | @$(INFO) installing husky $(HUSKY_VERSION) 10 | @mkdir -p $(TOOLS_HOST_DIR) 11 | @GOBIN=$(TOOLS_HOST_DIR) go install github.com/automation-co/husky@$(HUSKY_VERSION) 12 | @mv $(TOOLS_HOST_DIR)/husky $(HUSKY) 13 | @$(OK) installing husky $(HUSKY_VERSION) 14 | -------------------------------------------------------------------------------- /.mirrord/README.MD: -------------------------------------------------------------------------------- 1 | Currently outgoing network and filesystem are disabled, because we compile static binary and mirrord is not able to catch file system requests. 2 | Because it is't able to read resolver config properly, it is not able to resolve in-cluster host names. 3 | 4 | For more info please follow: https://github.com/metalbear-co/mirrord/issues/1922 5 | -------------------------------------------------------------------------------- /.mirrord/mirrord.json: -------------------------------------------------------------------------------- 1 | { 2 | "target": { 3 | "path": "deploy/provider-ceph-provider-cep", 4 | "namespace": "crossplane-system" 5 | }, 6 | "agent": { 7 | "log_level": "info", 8 | "namespace": "crossplane-system", 9 | "privileged": true 10 | }, 11 | "feature": { 12 | "network": { 13 | "incoming": "steal", 14 | "outgoing": false 15 | }, 16 | "fs": "local", 17 | "env": false 18 | }, 19 | "operator": false, 20 | "pause": true, 21 | "accept_invalid_certificates": true 22 | } 23 | -------------------------------------------------------------------------------- /.mirrord/mirrord.mk: -------------------------------------------------------------------------------- 1 | MIRRORD_VERSION ?= 3.88.0 2 | MIRRORD := $(TOOLS_HOST_DIR)/mirrord-$(MIRRORD_VERSION) 3 | 4 | # Best for development - locally run provider-ceph controller. 5 | mirrord.cluster: generate-pkg generate-tests crossplane-cluster localstack-cluster cert-manager load-package mirrord.certs 6 | @$(KUBECTL) apply -R -f package/crds 7 | @$(KUBECTL) apply -R -f package/webhookconfigurations 8 | $(KUBECTL) apply -f $(PROJECT_ROOT)/e2e/localstack/localstack-provider-cfg-host.yaml 9 | 10 | mirrord.certs: $(KUBECTL) 11 | @rm -rf $(PWD)/bin/certs ; mkdir $(PWD)/bin/certs 12 | @$(KUBECTL) get secret -n crossplane-system crossplane-provider-provider-ceph-server-cert -o 'go-template={{index .data "ca.crt"}}' | base64 -d >> $(PWD)/bin/certs/ca.crt 13 | @$(KUBECTL) get secret -n crossplane-system crossplane-provider-provider-ceph-server-cert -o 'go-template={{index .data "tls.crt"}}' | base64 -d >> $(PWD)/bin/certs/tls.crt 14 | @$(KUBECTL) get secret -n crossplane-system crossplane-provider-provider-ceph-server-cert -o 'go-template={{index .data "tls.key"}}' | base64 -d >> $(PWD)/bin/certs/tls.key 15 | @chmod 400 $(PWD)/bin/certs/*.crt 16 | 17 | mirrord.run: $(MIRRORD) 18 | @$(INFO) Starting mirrord on deployment 19 | $(MIRRORD) exec -f .mirrord/mirrord.json make run 20 | 21 | # Download mirrord locally if necessary. 22 | $(MIRRORD): 23 | @$(INFO) installing mirrord $(MIRRORD_VERSION) 24 | @mkdir -p $(TOOLS_HOST_DIR) || $(FAIL) 25 | @curl -fsSLo $(MIRRORD) https://github.com/metalbear-co/mirrord/releases/download/$(MIRRORD_VERSION)/mirrord_$(HOST_PLATFORM:-=_) || $(FAIL) 26 | @chmod +x $(MIRRORD) 27 | @$(OK) installing mirrord $(MIRRORD_VERSION) 28 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Community Code of Conduct 2 | 3 | This project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /OWNERS.md: -------------------------------------------------------------------------------- 1 | # OWNERS 2 | 3 | This page lists all maintainers for **this** repository. Each repository in the [Crossplane 4 | organization](https://github.com/crossplane/) will list their repository maintainers in their own 5 | `OWNERS.md` file. 6 | 7 | Please see the Crossplane 8 | [GOVERNANCE.md](https://github.com/crossplane/crossplane/blob/master/GOVERNANCE.md) for governance 9 | guidelines and responsibilities for the steering committee and maintainers. 10 | 11 | ## Maintainers 12 | 13 | * Nic Cope ([negz](https://github.com/negz)) 14 | * Daniel Mangum ([hasheddan](https://github.com/hasheddan)) 15 | * Muvaffak Onuş ([muvaf](https://github.com/muvaf)) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # provider-ceph 2 | 3 | `provider-ceph` is a minimal [Crossplane](https://crossplane.io/) Provider 4 | that reconciles `Bucket` CRs with multiple external S3 backends such as Ceph. It comes 5 | with the following features: 6 | 7 | - A `ProviderConfig` type that represents a single S3 backend (such as Ceph) and points to a credentials `Secret` for access to that backend. 8 | - A controller that reconciles `ProviderConfig` objects which represent S3 backends and stores client details for each backend. 9 | - A `Bucket` resource type that represents an S3 bucket. 10 | - A controller that observes `Bucket` objects and reconciles these objects with the S3 backends. 11 | 12 | ## Getting Started 13 | 14 | [Install Crossplane](https://docs.crossplane.io/v1.11/software/install/#install-crossplane) in you Kubernetes cluster 15 | 16 | Install the provider by using the Upbound CLI after changing the image tag to the latest release: 17 | 18 | ``` 19 | up ctp provider install xpkg.upbound.io/linode/provider-ceph:v1.0.5-rc.0.3.g72eb3c2 20 | ``` 21 | 22 | Alternatively, you can use declarative installation: 23 | ``` 24 | cat < [!WARNING] 17 | > It is the responsibility of the user/client to "unpause" a paused Bucket CR before performing an Update or Delete operation. 18 | 19 | ## Updating a Paused Bucket 20 | A paused Bucket CR can be updated like any other CR. However, the changes will _not_ trigger a reconciliation of the CR by Provider Ceph. To temporarily "unpause" a Bucket CR to allow Provider Ceph to reconcile an update, the Bucket CR pause label must be set to an empty string `""`. This will result in Provider Ceph reconciling an updated CR and then pausing the Bucket CR once again after the update is complete and the CR is considered `Synced`. 21 | 22 | ## Deleting a Paused Bucket 23 | To temporarily "unpause" a Bucket CR to allow Provider Ceph to perform Delete, the Bucket CR must be patched, setting the pause label to `"false"` or some other value that is **not** an empty string `""`. 24 | ``` 25 | kubectl patch bucket --type=merge -p '{"metadata":{"labels":{"crossplane.io/paused":"false"}}}' 26 | ``` 27 | The Bucket CR can then be deleted as normal: 28 | ``` 29 | kubectl delete bucket 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Refer to Crossplane's [CONTRIBUTING.md] file for more information on how the 4 | Crossplane community prefers to work. The [Provider Development][provider-dev] 5 | guide may also be of use. 6 | 7 | [CONTRIBUTING.md]: https://github.com/crossplane/crossplane/blob/master/CONTRIBUTING.md 8 | 9 | ## Running Locally 10 | Spin up the test environment, but with `provider-ceph` running locally in your terminal: 11 | 12 | ``` 13 | make dev 14 | ``` 15 | 16 | **or** 17 | 18 | 19 | Spin up the test environment, but without Localstack and use your own external Ceph cluster instead. Also with `provider-ceph` running locally in your terminal: 20 | 21 | ``` 22 | AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= CEPH_ADDRESS= make dev-ceph 23 | ``` 24 | 25 | In either case, after you've made some changes, kill (Ctrl+C) the existing `provider-ceph` and re-run it: 26 | 27 | ``` 28 | make run 29 | ``` 30 | 31 | ### Webhook Support 32 | Running the validation webhook locally is a bit tricky, but it works out of the box. 33 | Firt of all cluster provisioner script changes `ValidatingWebhookConfiguration`, to point to a 34 | [localtunnel](https://github.com/localtunnel/localtunnel) instance (created by the script). 35 | This endpoint has a valid TLS certification aprooved by Kubernetes, so validation requests should be served by the local process. 36 | 37 | ## Debugging Locally 38 | Spin up the test environment, but with `provider-ceph` running locally in your terminal: 39 | 40 | ``` 41 | make mirrord.cluster mirrord.run 42 | ``` 43 | 44 | For debugging please install `mirrord` plugin in your IDE of choice. 45 | 46 | ### Webhook Support 47 | Works out of the box. Validation requests goes to the original instance of the operator, but mirrord sends every network traffic to the local process instead. 48 | -------------------------------------------------------------------------------- /docs/RELEASE_PROCESS.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | ## Release Version 4 | The release process for `provider-ceph` *mostly* follows the process described [here](https://github.com/crossplane/release#tldr-process-overview). 5 | 6 | Here is a simplified set of steps to create a release for `provider-ceph`. 7 | 8 | 1. **feature freeze**: Merge all completed features into the main development branch to begin "feature freeze" period. 9 | 2. **pin dependencies**: Update the go module on the main development branch to depend on stable versions of dependencies if needed. 10 | 3. **tag release**: Run the **Tag** action on the main development branch with the desired version (e.g. `v0.0.2`). 11 | 1. The action will create a release branch (e.g. `release-v0.0.2`), update the controller version and README, and create a tag with the release branch. 12 | 2. The action also opens a PR against the main development branch. Please review/merge it to record the release. 13 | 4. **build/publish**: Run the **Publish** action on the release branch with the version that was just tagged. The released package will be published on the upbound marketplace [here](https://marketplace.upbound.io/account/linode/provider-ceph). 14 | 5. **tag next pre-release**: Run the **Tag Release Candidate** action on the main development branch with `-rc.0` for the next release (e.g. `v0.0.3-rc.0`). 15 | 16 | ## Release Candidate 17 | Every time there is a merge to `main`, the **Release Candidate Publish and PR** workflow will: 18 | 1. Create a release candidate package (eg `v0.0.3-rc.0.1.gcbf3f60is`) and publish it on the Upbound market place (Note: release candidates are not visible to the public on the Upbound marketplace). 19 | 2. Open a PR against the main development branch, updating the controller version and README to point at the latest release candidate. Please review/merge this PR to record the release (this PR will not trigger further CI workflows). 20 | -------------------------------------------------------------------------------- /docs/TESTING.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | ## Localstack 4 | Due to its lightweight nature, [LocalStack](https://localstack.cloud/) is used as the s3 backend for testing. 5 | 6 | A test setup with Localstack consists of the following: 7 | - A single [Kind](https://kind.sigs.k8s.io/) cluster with [Crossplane](https://www.crossplane.io/) installed and `provider-ceph` deployed. 8 | - Three [LocalStack](https://localstack.cloud/) instances deployed in the Kind cluster. 9 | 10 | The tests are run using [Chainsaw](https://kyverno.github.io/chainsaw/latest/) and s3 backend operations are verified using the [AWS CLI](https://aws.amazon.com/cli/). 11 | 12 | This is the test setup used by Github Actions for this repo. 13 | TODO: Update image as we no longer rely on Docker Compose. Localstack instances are now run within the Kind cluster. 14 | 15 | ![provider-ceph-testing drawio](https://user-images.githubusercontent.com/41484746/236199553-06990687-462a-4097-8d42-a7f7f055abbf.png) 16 | 17 | ## Run Chainsaw Test Suite Against Localstack 18 | 19 | ``` 20 | make chainsaw 21 | ``` 22 | 23 | ## Ceph 24 | A separate suite of tests can be run against a single Ceph cluster. These tests are not part of the Github Actions workflows for this repo. The Ceph cluster must be created separately and the keys & host base address are required as shown below. 25 | 26 | ## Run Chainsaw Test Suite Against Ceph 27 | 28 | ``` 29 | AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= CEPH_ADDRESS= make ceph-chainsaw 30 | 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/WEBHOOKS.md: -------------------------------------------------------------------------------- 1 | # Webhooks 2 | 3 | ## Enable Webhooks 4 | - Webhooks are enabled in Crossplane by default from `v1.13` onwards. For previous versions of Crossplane, include the flag `--set webhooks.enabled=true` when [installing Crossplane via Helm](https://docs.crossplane.io/v1.11/software/install/#install-the-crossplane-helm-chart). 5 | 6 | ## Bucket Admission Controlling Webhook 7 | Provider Ceph provides Dynamic Admission Control for Buckets. 8 | 9 | ### Bucket Validation Webhook 10 | Validates Bucket CRs for Create and Update operations. 11 | This webhook is also configured with an `objectSelector` label `provider-ceph.crossplane.io/validation-required: true`. 12 | It is the responsibility of the user (or the external system) to ensure that incoming Bucket CRs are given this label to enable webhook validation, should validation for the CR be desired. 13 | 14 | Create and Update operations on Buckets are blocked by the bucket admission webhook when: 15 | - The Bucket contains one or more providers (`bucket.spec.Providers`) that do not exist (i.e. a `ProviderConfig` of the same name does not exist in the k8s cluster). 16 | - Bucket Lifecycle Configurations cannot be validated against a backend. 17 | -------------------------------------------------------------------------------- /docs/debug/DEBUG_LOGS.md: -------------------------------------------------------------------------------- 1 | # Debug Logs 2 | 3 | Provider Ceph supports `--zap-log-level` and `--zap-stacktrace-level` flags. The easiest way to set flags on a provider is to create a DeploymentRuntimeConfig and reference it from the Provider. See `./provider.yaml` and `./deployment-runtime-config.yaml` as an example. 4 | -------------------------------------------------------------------------------- /docs/debug/deployment-runtime-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: pkg.crossplane.io/v1beta1 2 | kind: DeploymentRuntimeConfig 3 | metadata: 4 | name: debug-config 5 | spec: 6 | deploymentTemplate: 7 | spec: 8 | selector: {} 9 | template: 10 | spec: 11 | containers: 12 | - name: package-runtime 13 | args: 14 | - --zap-stacktrace-level=debug 15 | - --zap-log-level=debug 16 | -------------------------------------------------------------------------------- /docs/debug/provider.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: pkg.crossplane.io/v1 2 | kind: Provider 3 | metadata: 4 | name: linode-provider-ceph 5 | spec: 6 | package: xpkg.upbound.io/linode/provider-ceph: 7 | runtimeConfigRef: 8 | name: debug-config 9 | -------------------------------------------------------------------------------- /e2e/kind/kind-config-1.28.yaml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by hack/generate-tests.sh 2 | kind: Cluster 3 | apiVersion: kind.x-k8s.io/v1alpha4 4 | nodes: 5 | - role: control-plane 6 | image: kindest/node:v1.28.7 7 | extraPortMappings: 8 | - containerPort: 32566 9 | hostPort: 32566 10 | - containerPort: 32567 11 | hostPort: 32567 12 | - containerPort: 32568 13 | hostPort: 32568 14 | kubeadmConfigPatches: 15 | - | 16 | kind: ClusterConfiguration 17 | apiServer: 18 | extraArgs: 19 | max-mutating-requests-inflight: "2000" 20 | max-requests-inflight: "4000" 21 | -------------------------------------------------------------------------------- /e2e/kind/kind-config-1.29.yaml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by hack/generate-tests.sh 2 | kind: Cluster 3 | apiVersion: kind.x-k8s.io/v1alpha4 4 | nodes: 5 | - role: control-plane 6 | image: kindest/node:v1.29.2 7 | extraPortMappings: 8 | - containerPort: 32566 9 | hostPort: 32566 10 | - containerPort: 32567 11 | hostPort: 32567 12 | - containerPort: 32568 13 | hostPort: 32568 14 | kubeadmConfigPatches: 15 | - | 16 | kind: ClusterConfiguration 17 | apiServer: 18 | extraArgs: 19 | max-mutating-requests-inflight: "2000" 20 | max-requests-inflight: "4000" 21 | -------------------------------------------------------------------------------- /e2e/kind/kind-config-1.30.yaml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by hack/generate-tests.sh 2 | kind: Cluster 3 | apiVersion: kind.x-k8s.io/v1alpha4 4 | nodes: 5 | - role: control-plane 6 | image: kindest/node:v1.30.8 7 | extraPortMappings: 8 | - containerPort: 32566 9 | hostPort: 32566 10 | - containerPort: 32567 11 | hostPort: 32567 12 | - containerPort: 32568 13 | hostPort: 32568 14 | kubeadmConfigPatches: 15 | - | 16 | kind: ClusterConfiguration 17 | apiServer: 18 | extraArgs: 19 | max-mutating-requests-inflight: "2000" 20 | max-requests-inflight: "4000" 21 | -------------------------------------------------------------------------------- /e2e/kind/kind-config-1.31.yaml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by hack/generate-tests.sh 2 | kind: Cluster 3 | apiVersion: kind.x-k8s.io/v1alpha4 4 | nodes: 5 | - role: control-plane 6 | image: kindest/node:v1.31.4 7 | extraPortMappings: 8 | - containerPort: 32566 9 | hostPort: 32566 10 | - containerPort: 32567 11 | hostPort: 32567 12 | - containerPort: 32568 13 | hostPort: 32568 14 | kubeadmConfigPatches: 15 | - | 16 | kind: ClusterConfiguration 17 | apiServer: 18 | extraArgs: 19 | max-mutating-requests-inflight: "2000" 20 | max-requests-inflight: "4000" 21 | -------------------------------------------------------------------------------- /e2e/localstack/README.md: -------------------------------------------------------------------------------- 1 | Testing on Localstack 2 | 3 | Dependencies: 4 | 5 | * Kubectl 6 | 7 | Start Localstack: 8 | 9 | ```bash 10 | kubectl apply -f localstack-deployment.yaml 11 | ``` 12 | 13 | Apply provider config: 14 | 15 | ```bash 16 | kubectl apply -f localstack-provider-cfg.yaml 17 | ``` 18 | -------------------------------------------------------------------------------- /e2e/localstack/localstack-provider-cfg-host.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceph.crossplane.io/v1alpha1 2 | kind: ProviderConfig 3 | metadata: 4 | name: localstack-a 5 | spec: 6 | hostBase: "0.0.0.0:32566" 7 | credentials: 8 | source: Secret 9 | secretRef: 10 | namespace: crossplane-system 11 | name: localstack 12 | key: credentials 13 | disableHealthCheck: false 14 | --- 15 | apiVersion: ceph.crossplane.io/v1alpha1 16 | kind: ProviderConfig 17 | metadata: 18 | name: localstack-b 19 | spec: 20 | hostBase: "0.0.0.0:32567" 21 | credentials: 22 | source: Secret 23 | secretRef: 24 | namespace: crossplane-system 25 | name: localstack 26 | key: credentials 27 | disableHealthCheck: false 28 | --- 29 | apiVersion: ceph.crossplane.io/v1alpha1 30 | kind: ProviderConfig 31 | metadata: 32 | name: localstack-c 33 | spec: 34 | hostBase: "0.0.0.0:32568" 35 | credentials: 36 | source: Secret 37 | secretRef: 38 | namespace: crossplane-system 39 | name: localstack 40 | key: credentials 41 | disableHealthCheck: false 42 | -------------------------------------------------------------------------------- /e2e/localstack/localstack-provider-cfg.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceph.crossplane.io/v1alpha1 2 | kind: ProviderConfig 3 | metadata: 4 | name: localstack-a 5 | spec: 6 | hostBase: "localstack-a:32566" 7 | credentials: 8 | source: Secret 9 | secretRef: 10 | namespace: crossplane-system 11 | name: localstack 12 | key: credentials 13 | disableHealthCheck: false 14 | healthCheckIntervalSeconds: 5 15 | --- 16 | apiVersion: ceph.crossplane.io/v1alpha1 17 | kind: ProviderConfig 18 | metadata: 19 | name: localstack-b 20 | spec: 21 | hostBase: "localstack-b:32567" 22 | credentials: 23 | source: Secret 24 | secretRef: 25 | namespace: crossplane-system 26 | name: localstack 27 | key: credentials 28 | disableHealthCheck: false 29 | healthCheckIntervalSeconds: 5 30 | --- 31 | apiVersion: ceph.crossplane.io/v1alpha1 32 | kind: ProviderConfig 33 | metadata: 34 | name: localstack-c 35 | spec: 36 | hostBase: "localstack-c:32568" 37 | credentials: 38 | source: Secret 39 | secretRef: 40 | namespace: crossplane-system 41 | name: localstack 42 | key: credentials 43 | disableHealthCheck: false 44 | healthCheckIntervalSeconds: 5 45 | -------------------------------------------------------------------------------- /e2e/tests/ceph/.chainsaw.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/configuration-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Configuration 4 | metadata: 5 | creationTimestamp: null 6 | name: configuration 7 | spec: 8 | timeouts: 9 | apply: 120s 10 | assert: 120s 11 | cleanup: 120s 12 | delete: 120s 13 | error: 120s 14 | exec: 120s 15 | -------------------------------------------------------------------------------- /e2e/tests/stable/.chainsaw.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/configuration-chainsaw-v1alpha1.json 2 | apiVersion: chainsaw.kyverno.io/v1alpha1 3 | kind: Configuration 4 | metadata: 5 | creationTimestamp: null 6 | name: configuration 7 | spec: 8 | timeouts: 9 | apply: 30s 10 | assert: 30s 11 | cleanup: 15s 12 | delete: 30s 13 | error: 30s 14 | exec: 30s 15 | -------------------------------------------------------------------------------- /examples/provider/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: crossplane-system 5 | --- 6 | apiVersion: v1 7 | kind: Secret 8 | metadata: 9 | namespace: crossplane-system 10 | name: ceph-admin-cfg 11 | type: Opaque 12 | data: 13 | access_key: "RHVtbXk=" 14 | secret_key: "RHVtbXk=" 15 | --- 16 | apiVersion: ceph.crossplane.io/v1alpha1 17 | kind: ProviderConfig 18 | metadata: 19 | name: ceph-admin-cfg 20 | namespace: crossplane-system 21 | spec: 22 | hostBase: "localhost:4566" 23 | credentials: 24 | source: Secret 25 | secretRef: 26 | namespace: crossplane-system 27 | name: ceph-admin-cfg 28 | key: credentials 29 | -------------------------------------------------------------------------------- /examples/provider/provider.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: pkg.crossplane.io/v1 2 | kind: Provider 3 | metadata: 4 | name: provider-ceph 5 | spec: 6 | package: xpkg.upbound.io/linode/provider-ceph:v0.0.1 7 | -------------------------------------------------------------------------------- /examples/sample/bucket.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: provider-ceph.ceph.crossplane.io/v1alpha1 2 | kind: Bucket 3 | metadata: 4 | name: test-bucket 5 | spec: 6 | forProvider: {} 7 | -------------------------------------------------------------------------------- /examples/storeconfig/vault.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: ceph.crossplane.io/v1alpha1 2 | kind: StoreConfig 3 | metadata: 4 | name: vault 5 | spec: 6 | type: Vault 7 | defaultScope: crossplane-system 8 | vault: 9 | server: http://vault.vault-system:8200 10 | mountPath: secret/ 11 | version: v2 12 | auth: 13 | method: Token 14 | token: 15 | source: Secret 16 | secretRef: 17 | namespace: crossplane-system 18 | name: vault-token 19 | key: token 20 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane 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 | */ -------------------------------------------------------------------------------- /hack/deploy-provider.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : "${BUILD_REGISTRY?= required}" 4 | : "${PROJECT_NAME?= required}" 5 | : "${ARCH?= required}" 6 | : "${VERSION?= required}" 7 | 8 | # Apply a configuration for the provider deployment. 9 | kubectl apply -f - </dev/null; then 7 | # Resolve IP by node name. 8 | address="$(kubectl get no ${address%:*} -o jsonpath='{.status.addresses[0].address}'):${address#*:}" 9 | fi 10 | 11 | # Check whether the bucket already exists. 12 | # We suppress all output - we're interested only in the return code. 13 | 14 | bucket_exists() { 15 | aws --endpoint-url=http://$address s3api head-bucket \ 16 | --bucket $bucketname \ 17 | >/dev/null 2>&1 18 | 19 | if [[ ${?} -eq 0 ]]; then 20 | echo "pass: bucket $bucketname found" 21 | return 0 22 | else 23 | echo "error: bucket not found on $address" 24 | return 1 25 | fi 26 | } 27 | 28 | bucket_does_not_exist() { 29 | aws --endpoint-url=http://$address s3api head-bucket \ 30 | --bucket $bucketname \ 31 | >/dev/null 2>&1 32 | 33 | if [[ ${?} -eq 0 ]]; then 34 | echo "error: bucket found, should not exist" 35 | return 1 36 | else 37 | echo "pass: bucket does not exist" 38 | return 0 39 | fi 40 | } 41 | 42 | case "$1" in 43 | "") ;; 44 | bucket_exists) "$@"; exit;; 45 | bucket_does_not_exist) "$@"; exit;; 46 | 47 | *) echo "Unknown function: $1()"; exit 2;; 48 | esac 49 | -------------------------------------------------------------------------------- /hack/generate-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : "${TEST_KIND_NODES?= required}" 4 | : "${REPO?= required}" 5 | : "${LOCALSTACK_VERSION?= required}" 6 | 7 | # This script reads a comma-delimited string TEST_KIND_NODES of kind node versions 8 | # for chainsaw tests to be run on, and generates the relevant files for each version. 9 | 10 | IFS=', ' read -r -a kind_nodes <<< "$TEST_KIND_NODES" 11 | 12 | 13 | # remove existing files 14 | rm -f ./e2e/kind/* 15 | 16 | HEADER="# This file was auto-generated by hack/generate-tests.sh" 17 | 18 | for kind_node in "${kind_nodes[@]}" 19 | do 20 | # write kind config file for version 21 | major=${kind_node%.*} 22 | if [ ! -d "./e2e/kind" ]; then 23 | mkdir -p ./e2e/kind 24 | fi 25 | file=./e2e/kind/kind-config-${major}.yaml 26 | 27 | cat < "${file}" 28 | ${HEADER} 29 | kind: Cluster 30 | apiVersion: kind.x-k8s.io/v1alpha4 31 | nodes: 32 | - role: control-plane 33 | image: kindest/node:v${kind_node} 34 | extraPortMappings: 35 | - containerPort: 32566 36 | hostPort: 32566 37 | - containerPort: 32567 38 | hostPort: 32567 39 | - containerPort: 32568 40 | hostPort: 32568 41 | kubeadmConfigPatches: 42 | - | 43 | kind: ClusterConfiguration 44 | apiServer: 45 | extraArgs: 46 | max-mutating-requests-inflight: "2000" 47 | max-requests-inflight: "4000" 48 | EOF 49 | 50 | file=./.github/workflows/chainsaw-e2e-test-${major}.yaml 51 | 52 | cat < "${file}" 53 | ${HEADER} 54 | name: chainsaw e2e test ${major} 55 | on: [push] 56 | concurrency: 57 | group: chainsaw-${major}-\${{ github.ref }}-1 58 | cancel-in-progress: true 59 | permissions: 60 | contents: read 61 | jobs: 62 | test: 63 | name: chainsaw e2e test ${major} 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Cancel Previous Runs 67 | uses: styfle/cancel-workflow-action@0.9.1 68 | with: 69 | access_token: \${{ github.token }} 70 | 71 | - name: Checkout 72 | uses: actions/checkout@v4 73 | with: 74 | submodules: true 75 | 76 | - name: Setup Go 77 | uses: actions/setup-go@v5 78 | with: 79 | go-version: '1.23' 80 | 81 | - name: Vendor Dependencies 82 | run: make vendor vendor.check 83 | 84 | - name: Docker cache 85 | uses: ScribeMD/docker-cache@0.3.7 86 | with: 87 | key: docker-\${{ runner.os }}-\${{ hashFiles('go.sum') }}} 88 | 89 | - name: Run chainsaw tests ${major} 90 | run: make chainsaw 91 | env: 92 | LATEST_KUBE_VERSION: '${major}' 93 | AWS_ACCESS_KEY_ID: 'Dummy' 94 | AWS_SECRET_ACCESS_KEY: 'Dummy' 95 | AWS_DEFAULT_REGION: 'us-east-1' 96 | EOF 97 | 98 | done 99 | -------------------------------------------------------------------------------- /hack/helpers/addtype.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2022 The Crossplane Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Please set ProviderNameLower & ProviderNameUpper environment variables before running this script. 18 | # See: https://github.com/crossplane/terrajet/blob/main/docs/generating-a-provider.md 19 | set -euo pipefail 20 | 21 | APIVERSION="${APIVERSION:-v1alpha1}" 22 | echo "Adding type ${KIND} to group ${GROUP} with version ${APIVERSION}" 23 | 24 | export GROUP 25 | export KIND 26 | export APIVERSION 27 | export PROVIDER 28 | 29 | kind_lower=$(echo "${KIND}" | tr "[:upper:]" "[:lower:]") 30 | group_lower=$(echo "${GROUP}" | tr "[:upper:]" "[:lower:]") 31 | 32 | mkdir -p "apis/${group_lower}/${APIVERSION}" 33 | ${GOMPLATE} < "hack/helpers/apis/GROUP_LOWER/GROUP_LOWER.go.tmpl" > "apis/${group_lower}/${group_lower}.go" 34 | ${GOMPLATE} < "hack/helpers/apis/GROUP_LOWER/APIVERSION/KIND_LOWER_types.go.tmpl" > "apis/${group_lower}/${APIVERSION}/${kind_lower}_types.go" 35 | ${GOMPLATE} < "hack/helpers/apis/GROUP_LOWER/APIVERSION/doc.go.tmpl" > "apis/${group_lower}/${APIVERSION}/doc.go" 36 | ${GOMPLATE} < "hack/helpers/apis/GROUP_LOWER/APIVERSION/groupversion_info.go.tmpl" > "apis/${group_lower}/${APIVERSION}/groupversion_info.go" 37 | 38 | mkdir -p "internal/controller/${kind_lower}" 39 | ${GOMPLATE} < "hack/helpers/controller/KIND_LOWER/KIND_LOWER.go.tmpl" > "internal/controller/${kind_lower}/${kind_lower}.go" 40 | ${GOMPLATE} < "hack/helpers/controller/KIND_LOWER/KIND_LOWER_test.go.tmpl" > "internal/controller/${kind_lower}/${kind_lower}_test.go" 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /hack/helpers/apis/GROUP_LOWER/APIVERSION/KIND_LOWER_types.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Crossplane 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 {{ .Env.APIVERSION }} 18 | 19 | import ( 20 | "reflect" 21 | 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | 25 | xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 26 | ) 27 | 28 | // {{ .Env.KIND }}Parameters are the configurable fields of a {{ .Env.KIND }}. 29 | type {{ .Env.KIND }}Parameters struct { 30 | ConfigurableField string `json:"configurableField"` 31 | } 32 | 33 | // {{ .Env.KIND }}Observation are the observable fields of a {{ .Env.KIND }}. 34 | type {{ .Env.KIND }}Observation struct { 35 | ObservableField string `json:"observableField,omitempty"` 36 | } 37 | 38 | // A {{ .Env.KIND }}Spec defines the desired state of a {{ .Env.KIND }}. 39 | type {{ .Env.KIND }}Spec struct { 40 | xpv1.ResourceSpec `json:",inline"` 41 | ForProvider {{ .Env.KIND }}Parameters `json:"forProvider"` 42 | } 43 | 44 | // A {{ .Env.KIND }}Status represents the observed state of a {{ .Env.KIND }}. 45 | type {{ .Env.KIND }}Status struct { 46 | xpv1.ResourceStatus `json:",inline"` 47 | AtProvider {{ .Env.KIND }}Observation `json:"atProvider,omitempty"` 48 | } 49 | 50 | // +kubebuilder:object:root=true 51 | 52 | // A {{ .Env.KIND }} is an example API type. 53 | // +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" 54 | // +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" 55 | // +kubebuilder:printcolumn:name="EXTERNAL-NAME",type="string",JSONPath=".metadata.annotations.crossplane\\.io/external-name" 56 | // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" 57 | // +kubebuilder:subresource:status 58 | // +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,{{ .Env.PROVIDER | strings.ToLower }}} 59 | type {{ .Env.KIND }} struct { 60 | metav1.TypeMeta `json:",inline"` 61 | metav1.ObjectMeta `json:"metadata,omitempty"` 62 | 63 | Spec {{ .Env.KIND }}Spec `json:"spec"` 64 | Status {{ .Env.KIND }}Status `json:"status,omitempty"` 65 | } 66 | 67 | // +kubebuilder:object:root=true 68 | 69 | // {{ .Env.KIND }}List contains a list of {{ .Env.KIND }} 70 | type {{ .Env.KIND }}List struct { 71 | metav1.TypeMeta `json:",inline"` 72 | metav1.ListMeta `json:"metadata,omitempty"` 73 | Items []{{ .Env.KIND }} `json:"items"` 74 | } 75 | 76 | // {{ .Env.KIND }} type metadata. 77 | var ( 78 | {{ .Env.KIND }}Kind = reflect.TypeOf({{ .Env.KIND }}{}).Name() 79 | {{ .Env.KIND }}GroupKind = schema.GroupKind{Group: Group, Kind: {{ .Env.KIND }}Kind}.String() 80 | {{ .Env.KIND }}KindAPIVersion = {{ .Env.KIND }}Kind + "." + SchemeGroupVersion.String() 81 | {{ .Env.KIND }}GroupVersionKind = SchemeGroupVersion.WithKind({{ .Env.KIND }}Kind) 82 | ) 83 | 84 | func init() { 85 | SchemeBuilder.Register(&{{ .Env.KIND }}{}, &{{ .Env.KIND }}List{}) 86 | } 87 | -------------------------------------------------------------------------------- /hack/helpers/apis/GROUP_LOWER/APIVERSION/doc.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Crossplane 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 {{ .Env.APIVERSION | strings.ToLower }} 18 | -------------------------------------------------------------------------------- /hack/helpers/apis/GROUP_LOWER/APIVERSION/groupversion_info.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 The Crossplane 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 v1alpha1 contains the v1alpha1 group Sample resources of the {{ .Env.PROVIDER }} provider. 18 | // +kubebuilder:object:generate=true 19 | // +groupName={{ .Env.GROUP | strings.ToLower }}.{{ .Env.PROVIDER | strings.ToLower }}.crossplane.io 20 | // +versionName={{ .Env.APIVERSION | strings.ToLower }} 21 | package {{ .Env.APIVERSION | strings.ToLower }} 22 | 23 | import ( 24 | "k8s.io/apimachinery/pkg/runtime/schema" 25 | "sigs.k8s.io/controller-runtime/pkg/scheme" 26 | ) 27 | 28 | // Package type metadata. 29 | const ( 30 | Group = "{{ .Env.GROUP | strings.ToLower }}.{{ .Env.PROVIDER | strings.ToLower }}.crossplane.io" 31 | Version = "{{ .Env.APIVERSION | strings.ToLower }}" 32 | ) 33 | 34 | var ( 35 | // SchemeGroupVersion is group version used to register these objects 36 | SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version} 37 | 38 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 39 | SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} 40 | ) 41 | -------------------------------------------------------------------------------- /hack/helpers/apis/GROUP_LOWER/GROUP_LOWER.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Crossplane 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 {{ .Env.GROUP | strings.ToLower }} contains group {{ .Env.GROUP }} API versions 18 | package {{ .Env.GROUP | strings.ToLower }} 19 | -------------------------------------------------------------------------------- /hack/helpers/controller/KIND_LOWER/KIND_LOWER_test.go.tmpl: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Crossplane 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 {{ .Env.KIND | strings.ToLower }} 18 | 19 | import ( 20 | "context" 21 | "testing" 22 | 23 | "github.com/google/go-cmp/cmp" 24 | 25 | "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" 26 | "github.com/crossplane/crossplane-runtime/pkg/resource" 27 | "github.com/crossplane/crossplane-runtime/pkg/test" 28 | ) 29 | 30 | // Unlike many Kubernetes projects Crossplane does not use third party testing 31 | // libraries, per the common Go test review comments. Crossplane encourages the 32 | // use of table driven unit tests. The tests of the crossplane-runtime project 33 | // are representative of the testing style Crossplane encourages. 34 | // 35 | // https://github.com/golang/go/wiki/TestComments 36 | // https://github.com/crossplane/crossplane/blob/master/CONTRIBUTING.md#contributing-code 37 | 38 | func TestObserve(t *testing.T) { 39 | type fields struct { 40 | service interface{} 41 | } 42 | 43 | type args struct { 44 | ctx context.Context 45 | mg resource.Managed 46 | } 47 | 48 | type want struct { 49 | o managed.ExternalObservation 50 | err error 51 | } 52 | 53 | cases := map[string]struct { 54 | reason string 55 | fields fields 56 | args args 57 | want want 58 | }{ 59 | // TODO: Add test cases. 60 | } 61 | 62 | for name, tc := range cases { 63 | t.Run(name, func(t *testing.T) { 64 | e := external{service: tc.fields.service} 65 | got, err := e.Observe(tc.args.ctx, tc.args.mg) 66 | if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { 67 | t.Errorf("\n%s\ne.Observe(...): -want error, +got error:\n%s\n", tc.reason, diff) 68 | } 69 | if diff := cmp.Diff(tc.want.o, got); diff != "" { 70 | t.Errorf("\n%s\ne.Observe(...): -want, +got:\n%s\n", tc.reason, diff) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /hack/helpers/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2022 The Crossplane Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # Please set ProviderNameLower & ProviderNameUpper environment variables before running this script. 18 | # See: https://github.com/crossplane/terrajet/blob/main/docs/generating-a-provider.md 19 | set -euo pipefail 20 | 21 | ProviderNameUpper=${PROVIDER} 22 | ProviderNameLower=$(echo "${PROVIDER}" | tr "[:upper:]" "[:lower:]") 23 | 24 | git rm -r apis/sample 25 | git rm -r internal/controller/mytype 26 | 27 | REPLACE_FILES='./* ./.github :!build/** :!go.* :!hack/**' 28 | # shellcheck disable=SC2086 29 | git grep -l 'template' -- ${REPLACE_FILES} | xargs sed -i.bak "s/template/${ProviderNameLower}/g" 30 | # shellcheck disable=SC2086 31 | git grep -l 'Template' -- ${REPLACE_FILES} | xargs sed -i.bak "s/Template/${ProviderNameUpper}/g" 32 | # We need to be careful while replacing "template" keyword in go.mod as it could tamper 33 | # some imported packages under require section. 34 | sed -i.bak "s/provider-template/provider-${ProviderNameLower}/g" go.mod 35 | 36 | # Clean up the .bak files created by sed 37 | git clean -fd 38 | 39 | git mv "apis/template.go" "apis/${ProviderNameLower}.go" 40 | git mv "internal/controller/template.go" "internal/controller/${ProviderNameLower}.go" 41 | git mv "cluster/images/provider-template" "cluster/images/provider-${ProviderNameLower}" 42 | -------------------------------------------------------------------------------- /hack/install-pc-ceph-cluster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : "${AWS_ACCESS_KEY_ID?= required}" 4 | : "${AWS_SECRET_ACCESS_KEY?= required}" 5 | : "${CEPH_ADDRESS?= required}" 6 | 7 | encoded_access_key=$(echo -n ${AWS_ACCESS_KEY_ID} | base64) 8 | encoded_secret_key=$(echo -n ${AWS_SECRET_ACCESS_KEY} | base64) 9 | 10 | kubectl apply -f - <-- 45 | // provider-ceph-202312122T124851Z-VdlyVlHrWkDG5pQj 46 | func (r *roleSessionNameGenerator) generate(serviceName string) (string, error) { 47 | if len(serviceName) < roleSessionNameServiceNameMinLength { 48 | return "", errServiceNameRequired 49 | } 50 | if len(serviceName) > roleSessionNameServiceNameMaxLength { 51 | return "", errServiceNameTooLong 52 | } 53 | 54 | suffix, err := r.randomStringGenerator.Generate("", roleSessionNameSuffixLength, roleSessionNameSuffixCharset) 55 | if err != nil { 56 | return "", errors.Wrap(err, errSuffixGenerationFailed.Error()) 57 | } 58 | 59 | return fmt.Sprintf("%s-%s-%s", serviceName, r.now().Format(roleSessionNameTimestampFormat), suffix), nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/controller/s3clienthandler/role_session_name_test.go: -------------------------------------------------------------------------------- 1 | package s3clienthandler 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/crossplane/crossplane-runtime/pkg/errors" 8 | "github.com/linode/provider-ceph/internal/utils/randomstring/randomstringfakes" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestRoleSessionNameGenerator_Generate(t *testing.T) { 14 | t.Parallel() 15 | 16 | errRandom := errors.New("random error") 17 | now := func() time.Time { 18 | return time.Date(2023, 12, 11, 13, 39, 51, 0, time.UTC) 19 | } 20 | 21 | tests := map[string]struct { 22 | randomStringGenerator *randomstringfakes.FakeGenerator 23 | randomStringGeneratorAssertions func(t *testing.T, fake *randomstringfakes.FakeGenerator) 24 | 25 | serviceName string 26 | 27 | expected string 28 | expectedErr error 29 | }{ 30 | "good session name": { 31 | randomStringGenerator: func() *randomstringfakes.FakeGenerator { 32 | fake := &randomstringfakes.FakeGenerator{} 33 | fake.GenerateReturns("randomstring", nil) 34 | 35 | return fake 36 | }(), 37 | randomStringGeneratorAssertions: func(t *testing.T, fake *randomstringfakes.FakeGenerator) { 38 | t.Helper() 39 | require.Equal(t, 1, fake.GenerateCallCount()) 40 | prefix, length, charset := fake.GenerateArgsForCall(0) 41 | require.Equal(t, "", prefix) 42 | require.Equal(t, roleSessionNameSuffixLength, length) 43 | require.Same(t, charset, roleSessionNameSuffixCharset) 44 | }, 45 | serviceName: "bucketcrud", 46 | expected: "bucketcrud-20231211T133951Z-randomstring", 47 | }, 48 | "service name too short": { 49 | serviceName: "", 50 | expectedErr: errServiceNameRequired, 51 | }, 52 | "service name too long": { 53 | serviceName: string(make([]byte, roleSessionNameServiceNameMaxLength+1)), 54 | expectedErr: errServiceNameTooLong, 55 | }, 56 | "random suffix generation failed": { 57 | randomStringGenerator: func() *randomstringfakes.FakeGenerator { 58 | fake := &randomstringfakes.FakeGenerator{} 59 | fake.GenerateReturns("", errRandom) 60 | 61 | return fake 62 | }(), 63 | serviceName: "bucketcrud", 64 | expectedErr: errSuffixGenerationFailed, 65 | }, 66 | } 67 | 68 | for name, tt := range tests { 69 | t.Run(name, func(t *testing.T) { 70 | t.Parallel() 71 | 72 | generator := newRoleSessionNameGenerator() 73 | generator.now = now 74 | if tt.randomStringGenerator != nil { 75 | generator.randomStringGenerator = tt.randomStringGenerator 76 | } 77 | 78 | roleSessionName, err := generator.generate(tt.serviceName) 79 | 80 | if tt.randomStringGeneratorAssertions != nil { 81 | tt.randomStringGeneratorAssertions(t, tt.randomStringGenerator) 82 | } 83 | 84 | switch { 85 | case tt.expectedErr != nil: 86 | require.ErrorContains(t, err, tt.expectedErr.Error()) 87 | default: 88 | require.Equal(t, tt.expected, roleSessionName) 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /internal/features/features.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 The Crossplane 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 features 18 | 19 | import "github.com/crossplane/crossplane-runtime/pkg/feature" 20 | 21 | // Feature flags. 22 | // #nosec 23 | const ( 24 | // EnableAlphaExternalSecretStores enables alpha support for 25 | // External Secret Stores. See the below design for more details. 26 | // https://github.com/crossplane/crossplane/blob/390ddd/design/design-doc-external-secret-stores.md 27 | EnableAlphaExternalSecretStores feature.Flag = "EnableAlphaExternalSecretStores" 28 | 29 | // EnableAlphaManagementPolicies enables alpha support for 30 | // Management Policies. See the below design for more details. 31 | // https://github.com/crossplane/crossplane/blob/master/design/design-doc-observe-only-resources.md 32 | EnableAlphaManagementPolicies feature.Flag = "EnableAlphaManagementPolicies" 33 | ) 34 | -------------------------------------------------------------------------------- /internal/otel/otel.go: -------------------------------------------------------------------------------- 1 | // Package otel provides tools to interact with the opentelemetry golang sdk 2 | package otel 3 | 4 | import ( 5 | "context" 6 | "time" 7 | 8 | otelresource "go.opentelemetry.io/otel/sdk/resource" 9 | 10 | // We have to set this to match the version used in ~/go/pkg/mod/go.opentelemetry.io/otel/sdk 11 | // See https://github.com/open-telemetry/opentelemetry-go/issues/2341 12 | semconv "go.opentelemetry.io/otel/semconv/v1.26.0" 13 | ) 14 | 15 | const ( 16 | ServiceName = "provider-ceph" 17 | TimeoutGatherHostResources = time.Millisecond * 500 18 | ) 19 | 20 | // RuntimeResources creates an otel sdk resource struct describing the service 21 | // and runtime (host, process, runtime, etc). When used together with a 22 | // TracerProvider this data will be included in all traces created from it. 23 | func RuntimeResources() (*otelresource.Resource, error) { 24 | ctx, cancel := context.WithTimeout(context.Background(), TimeoutGatherHostResources) 25 | defer cancel() 26 | runtimeResources, err := otelresource.New( 27 | ctx, 28 | otelresource.WithOSDescription(), // I.E. "Ubuntu 20.04.6 LTS (Focal Fossa) (Linux bos-lhvxje 5.15.0-60-generic #66~20.04.1-Ubuntu SMP Wed Jan 25 09:41:30 UTC 2023 x86_64)" 29 | otelresource.WithProcessRuntimeDescription(), // I.E. "go version go1.20.3 linux/amd64" 30 | otelresource.WithHost(), // I.E. In k8s this is the pod name 31 | otelresource.WithSchemaURL(semconv.SchemaURL), 32 | otelresource.WithAttributes( 33 | semconv.ServiceName(ServiceName), 34 | ), 35 | ) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return runtimeResources, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/otel/traces/traces.go: -------------------------------------------------------------------------------- 1 | // Package traces provides helper functions for tracing with OpenTelemetry SDK. 2 | package traces 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/go-logr/logr" 10 | "github.com/linode/provider-ceph/internal/consts" 11 | "github.com/linode/provider-ceph/internal/otel" 12 | 13 | otelsdk "go.opentelemetry.io/otel" 14 | otelcodes "go.opentelemetry.io/otel/codes" 15 | "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" 16 | "go.opentelemetry.io/otel/propagation" 17 | otelsdktrace "go.opentelemetry.io/otel/sdk/trace" 18 | "go.opentelemetry.io/otel/trace" 19 | "google.golang.org/grpc" 20 | "google.golang.org/grpc/credentials/insecure" 21 | ) 22 | 23 | type ctxKeyLogger struct{} 24 | 25 | // InjectTraceAndLogger returns a context and logger enriched with trace ID (if available). 26 | // If a logger already exists in the context, it returns it directly. 27 | func InjectTraceAndLogger(ctx context.Context, baseLogger logr.Logger) (context.Context, logr.Logger) { 28 | if logger, ok := ctx.Value(ctxKeyLogger{}).(logr.Logger); ok { 29 | return ctx, logger 30 | } 31 | 32 | span := trace.SpanFromContext(ctx) 33 | if span.SpanContext().IsValid() { 34 | baseLogger = baseLogger.WithValues(consts.TraceID, span.SpanContext().TraceID().String()) 35 | } 36 | 37 | return context.WithValue(ctx, ctxKeyLogger{}, baseLogger), baseLogger 38 | } 39 | 40 | // InitTracerProvider configures a global tracer provider and dials to the OTEL Collector. 41 | // Failing in doing so returns an error since service actively export their traces and 42 | // require the Collector to be up. 43 | // Returns a shutdown function that should be called at the end of the program to flush 44 | // all in-momory traces. 45 | func InitTracerProvider(log logr.Logger, otelCollectorAddress string, dialTimeout, exportInterval time.Duration) (func(context.Context), error) { 46 | runtimeResources, err := otel.RuntimeResources() 47 | if err != nil { 48 | return nil, fmt.Errorf("failed to gather runtime resources for traces provider: %w", err) 49 | } 50 | 51 | ctx, cancel := context.WithTimeout(context.Background(), dialTimeout) 52 | defer cancel() 53 | 54 | conn, err := grpc.NewClient(otelCollectorAddress, 55 | grpc.WithTransportCredentials(insecure.NewCredentials()), 56 | ) 57 | if err != nil { 58 | return nil, fmt.Errorf("failed to create gRPC connection to otel collector: %w", err) 59 | } 60 | 61 | // Set up a tracer exporter 62 | traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn)) 63 | if err != nil { 64 | return nil, fmt.Errorf("failed to create traces exporter: %w", err) 65 | } 66 | 67 | tp := otelsdktrace.NewTracerProvider( 68 | otelsdktrace.WithBatcher(traceExporter, otelsdktrace.WithBatchTimeout(exportInterval)), 69 | otelsdktrace.WithResource(runtimeResources), 70 | ) 71 | otelsdk.SetTracerProvider(tp) 72 | otelsdk.SetTextMapPropagator(propagation.TraceContext{}) 73 | 74 | flushFunction := func(ctx context.Context) { 75 | if err := tp.Shutdown(ctx); err != nil { 76 | log.Error(err, "failed to shutdown tracer provider and flush in-memory records") 77 | } 78 | } 79 | 80 | return flushFunction, nil 81 | } 82 | 83 | func SetAndRecordError(span trace.Span, err error) { 84 | span.SetStatus(otelcodes.Error, err.Error()) 85 | span.RecordError(err) 86 | } 87 | -------------------------------------------------------------------------------- /internal/otel/traces/traces_test.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/go-logr/logr/testr" 8 | "github.com/stretchr/testify/assert" 9 | "go.opentelemetry.io/otel/sdk/trace" 10 | "go.opentelemetry.io/otel/sdk/trace/tracetest" 11 | ) 12 | 13 | func TestInjectTraceAndLogger_NoTrace(t *testing.T) { 14 | t.Parallel() 15 | 16 | baseLogger := testr.New(t) 17 | ctx := context.Background() 18 | 19 | _, logger := InjectTraceAndLogger(ctx, baseLogger) 20 | assert.NotNil(t, logger) 21 | 22 | // Should still be the same base logger since no trace 23 | assert.Equal(t, baseLogger, logger) 24 | } 25 | 26 | func TestInjectTraceAndLogger_WithValidTrace(t *testing.T) { 27 | t.Parallel() 28 | 29 | baseLogger := testr.New(t) 30 | 31 | // Setup a test tracer provider 32 | sr := tracetest.NewSpanRecorder() 33 | tp := trace.NewTracerProvider() 34 | tp.RegisterSpanProcessor(sr) 35 | tracer := tp.Tracer("test") 36 | 37 | // Create a span 38 | ctx, span := tracer.Start(context.Background(), "test-span") 39 | defer span.End() 40 | 41 | _, logger := InjectTraceAndLogger(ctx, baseLogger) 42 | assert.NotNil(t, logger) 43 | assert.NotEqual(t, baseLogger, logger) 44 | 45 | // Span context should be valid 46 | sc := span.SpanContext() 47 | assert.True(t, sc.IsValid()) 48 | } 49 | 50 | func TestInjectTraceAndLogger_ReusesLoggerFromContext(t *testing.T) { 51 | t.Parallel() 52 | 53 | baseLogger := testr.New(t) 54 | ctx := context.WithValue(context.Background(), ctxKeyLogger{}, baseLogger) 55 | 56 | // Inject the logger, should reuse the existing logger from context 57 | _, logger := InjectTraceAndLogger(ctx, testr.New(t)) 58 | 59 | // Should return the original logger from the context 60 | assert.Equal(t, baseLogger, logger) 61 | } 62 | -------------------------------------------------------------------------------- /internal/rgw/acl.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "context" 5 | 6 | awss3 "github.com/aws/aws-sdk-go-v2/service/s3" 7 | "github.com/crossplane/crossplane-runtime/pkg/errors" 8 | "github.com/linode/provider-ceph/internal/backendstore" 9 | "github.com/linode/provider-ceph/internal/otel/traces" 10 | "go.opentelemetry.io/otel" 11 | ) 12 | 13 | const ( 14 | errGetBucketACL = "failed to get bucket acl" 15 | errPutBucketACL = "failed to put bucket acl" 16 | ) 17 | 18 | func GetBucketAcl(ctx context.Context, s3Backend backendstore.S3Client, in *awss3.GetBucketAclInput, o ...func(*awss3.Options)) (*awss3.GetBucketAclOutput, error) { 19 | ctx, span := otel.Tracer("").Start(ctx, "GetBucketAcl") 20 | defer span.End() 21 | 22 | resp, err := s3Backend.GetBucketAcl(ctx, in, o...) 23 | if err != nil { 24 | traces.SetAndRecordError(span, err) 25 | 26 | return resp, errors.Wrap(err, errGetBucketACL) 27 | } 28 | 29 | return resp, err 30 | } 31 | 32 | func PutBucketAcl(ctx context.Context, s3Backend backendstore.S3Client, in *awss3.PutBucketAclInput, o ...func(*awss3.Options)) (*awss3.PutBucketAclOutput, error) { 33 | ctx, span := otel.Tracer("").Start(ctx, "PutBucketAcl") 34 | defer span.End() 35 | 36 | resp, err := s3Backend.PutBucketAcl(ctx, in, o...) 37 | if err != nil { 38 | traces.SetAndRecordError(span, err) 39 | 40 | return resp, errors.Wrap(err, errPutBucketACL) 41 | } 42 | 43 | return resp, err 44 | } 45 | -------------------------------------------------------------------------------- /internal/rgw/acl_helpers.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go-v2/aws" 5 | "github.com/aws/aws-sdk-go-v2/service/s3" 6 | s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" 7 | "github.com/linode/provider-ceph/apis/provider-ceph/v1alpha1" 8 | ) 9 | 10 | func BucketToPutBucketACLInput(bucket *v1alpha1.Bucket) *s3.PutBucketAclInput { 11 | putBucketAclInput := &s3.PutBucketAclInput{} 12 | putBucketAclInput.ACL = s3types.BucketCannedACL(aws.ToString(bucket.Spec.ForProvider.ACL)) 13 | putBucketAclInput.Bucket = aws.String(bucket.Name) 14 | putBucketAclInput.GrantFullControl = bucket.Spec.ForProvider.GrantFullControl 15 | putBucketAclInput.GrantRead = bucket.Spec.ForProvider.GrantRead 16 | putBucketAclInput.GrantReadACP = bucket.Spec.ForProvider.GrantReadACP 17 | putBucketAclInput.GrantWrite = bucket.Spec.ForProvider.GrantWrite 18 | putBucketAclInput.GrantWriteACP = bucket.Spec.ForProvider.GrantWriteACP 19 | 20 | if bucket.Spec.ForProvider.AccessControlPolicy != nil { 21 | aclPolicy := &s3types.AccessControlPolicy{} 22 | if bucket.Spec.ForProvider.AccessControlPolicy.Grants != nil { 23 | aclPolicy.Grants = GenerateGrants(bucket.Spec.ForProvider.AccessControlPolicy.Grants) 24 | } 25 | if bucket.Spec.ForProvider.AccessControlPolicy.Owner != nil { 26 | aclPolicy.Owner = GenerateOwner(bucket.Spec.ForProvider.AccessControlPolicy.Owner) 27 | } 28 | 29 | putBucketAclInput.AccessControlPolicy = aclPolicy 30 | } 31 | 32 | return putBucketAclInput 33 | } 34 | 35 | func GenerateAccessControlPolicy(policyIn *v1alpha1.AccessControlPolicy) *s3types.AccessControlPolicy { 36 | return &s3types.AccessControlPolicy{ 37 | Grants: GenerateGrants(policyIn.Grants), 38 | Owner: GenerateOwner(policyIn.Owner), 39 | } 40 | } 41 | 42 | func GenerateGrants(grantsIn []v1alpha1.Grant) []s3types.Grant { 43 | grantsOut := make([]s3types.Grant, 0) 44 | 45 | for _, grantIn := range grantsIn { 46 | localGrant := s3types.Grant{} 47 | if grantIn.Grantee != nil { 48 | localGrant.Grantee = &s3types.Grantee{} 49 | if grantIn.Grantee.DisplayName != nil { 50 | localGrant.Grantee.DisplayName = grantIn.Grantee.DisplayName 51 | } 52 | if grantIn.Grantee.EmailAddress != nil { 53 | localGrant.Grantee.EmailAddress = grantIn.Grantee.EmailAddress 54 | } 55 | if grantIn.Grantee.ID != nil { 56 | localGrant.Grantee.ID = grantIn.Grantee.ID 57 | } 58 | if grantIn.Grantee.URI != nil { 59 | localGrant.Grantee.URI = grantIn.Grantee.URI 60 | } 61 | localGrant.Permission = s3types.Permission(grantIn.Permission) 62 | // Type is required. 63 | localGrant.Grantee.Type = s3types.Type(grantIn.Grantee.Type) 64 | } 65 | grantsOut = append(grantsOut, localGrant) 66 | } 67 | 68 | return grantsOut 69 | } 70 | 71 | func GenerateOwner(ownerIn *v1alpha1.Owner) *s3types.Owner { 72 | ownerOut := &s3types.Owner{} 73 | if ownerIn.DisplayName != nil { 74 | ownerOut.DisplayName = ownerIn.DisplayName 75 | } 76 | if ownerIn.ID != nil { 77 | ownerOut.ID = ownerIn.ID 78 | } 79 | 80 | return ownerOut 81 | } 82 | 83 | func BucketToGetBucketACLInput(bucket *v1alpha1.Bucket) *s3.GetBucketAclInput { 84 | return &s3.GetBucketAclInput{ 85 | Bucket: aws.String(bucket.Name), 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/rgw/assumerole.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/service/sts" 7 | "github.com/crossplane/crossplane-runtime/pkg/errors" 8 | "github.com/linode/provider-ceph/internal/backendstore" 9 | "github.com/linode/provider-ceph/internal/otel/traces" 10 | "go.opentelemetry.io/otel" 11 | ) 12 | 13 | const ( 14 | errAssumeRole = "failed to assume role" 15 | ) 16 | 17 | func AssumeRole(ctx context.Context, stsClient backendstore.STSClient, input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) { 18 | ctx, span := otel.Tracer("").Start(ctx, "AssumeRole") 19 | defer span.End() 20 | 21 | resp, err := stsClient.AssumeRole(ctx, input) 22 | if err != nil { 23 | err = errors.Wrap(err, errAssumeRole) 24 | traces.SetAndRecordError(span, err) 25 | 26 | return resp, err 27 | } 28 | 29 | return resp, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/rgw/bucket_helpers.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go-v2/aws" 5 | "github.com/aws/aws-sdk-go-v2/service/s3" 6 | s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" 7 | "github.com/aws/smithy-go" 8 | "github.com/crossplane/crossplane-runtime/pkg/errors" 9 | "github.com/linode/provider-ceph/apis/provider-ceph/v1alpha1" 10 | ) 11 | 12 | func BucketToCreateBucketInput(bucket *v1alpha1.Bucket) *s3.CreateBucketInput { 13 | createBucketInput := &s3.CreateBucketInput{ 14 | ACL: s3types.BucketCannedACL(aws.ToString(bucket.Spec.ForProvider.ACL)), 15 | Bucket: aws.String(bucket.Name), 16 | GrantFullControl: bucket.Spec.ForProvider.GrantFullControl, 17 | GrantRead: bucket.Spec.ForProvider.GrantRead, 18 | GrantReadACP: bucket.Spec.ForProvider.GrantReadACP, 19 | GrantWrite: bucket.Spec.ForProvider.GrantWrite, 20 | GrantWriteACP: bucket.Spec.ForProvider.GrantWriteACP, 21 | ObjectLockEnabledForBucket: bucket.Spec.ForProvider.ObjectLockEnabledForBucket, 22 | ObjectOwnership: s3types.ObjectOwnership(aws.ToString(bucket.Spec.ForProvider.ObjectOwnership)), 23 | } 24 | 25 | if bucket.Spec.ForProvider.LocationConstraint != "" { 26 | createBucketInput.CreateBucketConfiguration = &s3types.CreateBucketConfiguration{ 27 | LocationConstraint: s3types.BucketLocationConstraint(bucket.Spec.ForProvider.LocationConstraint), 28 | } 29 | } 30 | 31 | return createBucketInput 32 | } 33 | 34 | func BucketToPutBucketOwnershipControlsInput(bucket *v1alpha1.Bucket) *s3.PutBucketOwnershipControlsInput { 35 | return &s3.PutBucketOwnershipControlsInput{ 36 | Bucket: aws.String(bucket.Name), 37 | OwnershipControls: &s3types.OwnershipControls{ 38 | Rules: []s3types.OwnershipControlsRule{ 39 | { 40 | ObjectOwnership: s3types.ObjectOwnership(aws.ToString(bucket.Spec.ForProvider.ObjectOwnership)), 41 | }, 42 | }, 43 | }, 44 | } 45 | } 46 | 47 | // IsAlreadyExists helper function to test for ErrCodeBucketAlreadyExists error 48 | func IsAlreadyExists(err error) bool { 49 | var alreadyExists *s3types.BucketAlreadyExists 50 | 51 | return errors.As(err, &alreadyExists) 52 | } 53 | 54 | // IsAlreadyOwnedByYou helper function to test for ErrCodeBucketAlreadyOwnedByYou error 55 | func IsAlreadyOwnedByYou(err error) bool { 56 | var alreadyOwnedByYou *s3types.BucketAlreadyOwnedByYou 57 | 58 | return errors.As(err, &alreadyOwnedByYou) 59 | } 60 | 61 | // IsNotFound helper function to test for NotFound error 62 | func IsNotFound(err error) bool { 63 | var notFoundError *s3types.NotFound 64 | 65 | return errors.As(err, ¬FoundError) 66 | } 67 | 68 | // NoSuchBucket helper function to test for NoSuchBucket error 69 | func NoSuchBucket(err error) bool { 70 | var noSuchBucketError *s3types.NoSuchBucket 71 | 72 | return errors.As(err, &noSuchBucketError) 73 | } 74 | 75 | func IsNotEmpty(err error) bool { 76 | var ae smithy.APIError 77 | if !errors.As(err, &ae) { 78 | return false 79 | } 80 | 81 | return ae != nil && ae.ErrorCode() == "BucketNotEmpty" 82 | } 83 | 84 | // Unlike NoSuchBucket error or others, aws-sdk-go-v2 doesn't have a specific struct definition for BucketNotEmpty error. 85 | // So we should define ourselves. This is currently only for testing. 86 | type BucketNotEmptyError struct{} 87 | 88 | func (e BucketNotEmptyError) Error() string { 89 | return "BucketNotEmpty: some error" 90 | } 91 | 92 | func (e BucketNotEmptyError) ErrorCode() string { 93 | return "BucketNotEmpty" 94 | } 95 | 96 | func (e BucketNotEmptyError) ErrorMessage() string { 97 | return "some error" 98 | } 99 | 100 | func (e BucketNotEmptyError) ErrorFault() smithy.ErrorFault { 101 | return smithy.FaultUnknown 102 | } 103 | -------------------------------------------------------------------------------- /internal/rgw/bucket_helpers_test.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "testing" 5 | 6 | s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" 7 | "github.com/crossplane/crossplane-runtime/pkg/errors" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestIsNotEmpty(t *testing.T) { 12 | t.Parallel() 13 | 14 | testCases := map[string]struct { 15 | err error 16 | expected bool 17 | }{ 18 | "true - BucketNotEmpty error": { 19 | err: BucketNotEmptyError{}, 20 | expected: true, 21 | }, 22 | "false - Error not implement AWS API error": { 23 | err: errors.New("some error"), 24 | expected: false, 25 | }, 26 | "false - Other AWS API error": { 27 | err: &s3types.NoSuchBucket{}, 28 | expected: false, 29 | }, 30 | } 31 | 32 | for name, tt := range testCases { 33 | t.Run(name, func(t *testing.T) { 34 | t.Parallel() 35 | 36 | actual := IsNotEmpty(tt.err) 37 | 38 | assert.Equal(t, tt.expected, actual, "result does not match") 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/rgw/bucket_test.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go-v2/service/s3" 8 | s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" 9 | "github.com/crossplane/crossplane-runtime/pkg/errors" 10 | "github.com/linode/provider-ceph/internal/backendstore/backendstorefakes" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestDeleteBucket(t *testing.T) { 15 | t.Parallel() 16 | 17 | testCases := map[string]struct { 18 | s3BackendFunc func(err error) *backendstorefakes.FakeS3Client 19 | healthCheck bool 20 | expectedErr error 21 | }{ 22 | "ok - non-healthcheck bucket": { 23 | s3BackendFunc: func(err error) *backendstorefakes.FakeS3Client { 24 | fake := &backendstorefakes.FakeS3Client{} 25 | fake.HeadBucketReturns(nil, nil) 26 | 27 | fake.DeleteBucketReturns(nil, nil) 28 | 29 | return fake 30 | }, 31 | }, 32 | "ok - healthcheck bucket": { 33 | s3BackendFunc: func(err error) *backendstorefakes.FakeS3Client { 34 | fake := &backendstorefakes.FakeS3Client{} 35 | fake.HeadBucketReturns(nil, nil) 36 | 37 | isTruncated := false 38 | fake.ListObjectsV2Returns(&s3.ListObjectsV2Output{IsTruncated: &isTruncated}, nil) 39 | fake.ListObjectVersionsReturns(&s3.ListObjectVersionsOutput{IsTruncated: &isTruncated}, nil) 40 | 41 | fake.DeleteBucketReturns(nil, nil) 42 | 43 | return fake 44 | }, 45 | healthCheck: true, 46 | }, 47 | "ok - bucket does not exist": { 48 | s3BackendFunc: func(err error) *backendstorefakes.FakeS3Client { 49 | fake := &backendstorefakes.FakeS3Client{} 50 | fake.HeadBucketReturns(nil, &s3types.NotFound{}) 51 | 52 | return fake 53 | }, 54 | }, 55 | "bucketExists returns unexpected error": { 56 | s3BackendFunc: func(err error) *backendstorefakes.FakeS3Client { 57 | fake := &backendstorefakes.FakeS3Client{} 58 | fake.HeadBucketReturns(nil, err) 59 | 60 | return fake 61 | }, 62 | expectedErr: errors.New(errHeadBucket), 63 | }, 64 | "delete objects returns error": { 65 | s3BackendFunc: func(err error) *backendstorefakes.FakeS3Client { 66 | fake := &backendstorefakes.FakeS3Client{} 67 | fake.HeadBucketReturns(nil, nil) 68 | 69 | fake.ListObjectsV2Returns(nil, err) 70 | isTruncated := false 71 | fake.ListObjectVersionsReturns(&s3.ListObjectVersionsOutput{IsTruncated: &isTruncated}, nil) 72 | 73 | return fake 74 | }, 75 | healthCheck: true, 76 | expectedErr: errors.New(errListObjects), 77 | }, 78 | "delete object versions returns error": { 79 | s3BackendFunc: func(err error) *backendstorefakes.FakeS3Client { 80 | fake := &backendstorefakes.FakeS3Client{} 81 | fake.HeadBucketReturns(nil, nil) 82 | 83 | isTruncated := false 84 | fake.ListObjectsV2Returns(&s3.ListObjectsV2Output{IsTruncated: &isTruncated}, nil) 85 | fake.ListObjectVersionsReturns(nil, err) 86 | 87 | return fake 88 | }, 89 | healthCheck: true, 90 | expectedErr: errors.New(errListObjectVersions), 91 | }, 92 | "bucket not empty error": { 93 | s3BackendFunc: func(err error) *backendstorefakes.FakeS3Client { 94 | fake := &backendstorefakes.FakeS3Client{} 95 | fake.HeadBucketReturns(nil, nil) 96 | 97 | fake.DeleteBucketReturns(nil, BucketNotEmptyError{}) 98 | 99 | return fake 100 | }, 101 | expectedErr: ErrBucketNotEmpty, 102 | }, 103 | "other error during backend bucket deletion": { 104 | s3BackendFunc: func(err error) *backendstorefakes.FakeS3Client { 105 | fake := &backendstorefakes.FakeS3Client{} 106 | fake.HeadBucketReturns(nil, nil) 107 | 108 | fake.DeleteBucketReturns(nil, err) 109 | 110 | return fake 111 | }, 112 | expectedErr: errors.New(errDeleteBucket), 113 | }, 114 | } 115 | 116 | bucketName := "test-bucket" 117 | 118 | for name, tt := range testCases { 119 | t.Run(name, func(t *testing.T) { 120 | t.Parallel() 121 | 122 | client := tt.s3BackendFunc(tt.expectedErr) 123 | 124 | err := DeleteBucket(context.Background(), client, &bucketName, tt.healthCheck) 125 | 126 | if tt.expectedErr != nil { 127 | assert.ErrorIs(t, err, tt.expectedErr, "error does not match") 128 | } else { 129 | assert.NoError(t, err, "unexpected error") 130 | } 131 | }) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /internal/rgw/client.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/config" 10 | "github.com/aws/smithy-go/middleware" 11 | smithyhttp "github.com/aws/smithy-go/transport/http" 12 | "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 13 | 14 | "k8s.io/client-go/util/retry" 15 | 16 | "github.com/aws/aws-sdk-go-v2/credentials" 17 | "github.com/aws/aws-sdk-go-v2/service/s3" 18 | "github.com/aws/aws-sdk-go-v2/service/sts" 19 | 20 | apisv1alpha1 "github.com/linode/provider-ceph/apis/v1alpha1" 21 | "github.com/linode/provider-ceph/internal/consts" 22 | "github.com/linode/provider-ceph/internal/utils" 23 | ) 24 | 25 | const ( 26 | defaultRegion = "us-east-1" 27 | ) 28 | 29 | func NewS3Client(ctx context.Context, data map[string][]byte, pcSpec *apisv1alpha1.ProviderConfigSpec, s3Timeout time.Duration, sessionToken *string) (*s3.Client, error) { 30 | sessionConfig, err := buildSessionConfig(ctx, data) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | resolvedAddress := utils.ResolveHostBase(pcSpec.HostBase, pcSpec.UseHTTPS) 36 | 37 | return s3.NewFromConfig(sessionConfig, func(o *s3.Options) { 38 | o.UsePathStyle = true 39 | o.HTTPClient = &http.Client{ 40 | Timeout: s3Timeout, 41 | Transport: otelhttp.NewTransport(http.DefaultTransport), 42 | } 43 | o.BaseEndpoint = &resolvedAddress 44 | if sessionToken != nil { 45 | o.APIOptions = []func(*middleware.Stack) error{ 46 | smithyhttp.AddHeaderValue(consts.KeySecurityToken, *sessionToken), 47 | } 48 | } 49 | }), nil 50 | } 51 | 52 | func NewSTSClient(ctx context.Context, data map[string][]byte, pcSpec *apisv1alpha1.ProviderConfigSpec, s3Timeout time.Duration) (*sts.Client, error) { 53 | // If an STSAddress has not been set in the ProviderConfig Spec, use the HostBase. 54 | // The STSAddress is only necessary if we wish to contact an STS compliant authentication 55 | // service separate to the HostBase (i.e RGW address). 56 | stsAddress := pcSpec.STSAddress 57 | if stsAddress == nil { 58 | stsAddress = &pcSpec.HostBase 59 | } 60 | 61 | sessionConfig, err := buildSessionConfig(ctx, data) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | resolvedAddress := utils.ResolveHostBase(*stsAddress, pcSpec.UseHTTPS) 67 | 68 | return sts.NewFromConfig(sessionConfig, func(o *sts.Options) { 69 | o.HTTPClient = &http.Client{ 70 | Timeout: s3Timeout, 71 | Transport: otelhttp.NewTransport(http.DefaultTransport), 72 | } 73 | o.BaseEndpoint = &resolvedAddress 74 | }), nil 75 | } 76 | 77 | func buildSessionConfig(ctx context.Context, data map[string][]byte) (aws.Config, error) { 78 | return config.LoadDefaultConfig(ctx, 79 | config.WithRetryMaxAttempts(retry.DefaultRetry.Steps), 80 | config.WithRetryMode(aws.RetryModeStandard), 81 | config.WithRegion(defaultRegion), 82 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( 83 | string(data[consts.KeyAccessKey]), 84 | string(data[consts.KeySecretKey]), 85 | "", 86 | ))) 87 | } 88 | -------------------------------------------------------------------------------- /internal/rgw/lifecycleconfig.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "context" 5 | 6 | awss3 "github.com/aws/aws-sdk-go-v2/service/s3" 7 | "github.com/crossplane/crossplane-runtime/pkg/errors" 8 | "github.com/crossplane/crossplane-runtime/pkg/resource" 9 | "go.opentelemetry.io/otel" 10 | 11 | "github.com/linode/provider-ceph/apis/provider-ceph/v1alpha1" 12 | "github.com/linode/provider-ceph/internal/backendstore" 13 | "github.com/linode/provider-ceph/internal/otel/traces" 14 | ) 15 | 16 | const ( 17 | errGetLifecycleConfig = "failed to get bucket lifecycle configuration" 18 | errPutLifecycleConfig = "failed to put bucket lifecycle configuration" 19 | errDeleteLifecycle = "failed to delete bucket lifecycle" 20 | ) 21 | 22 | func PutBucketLifecycleConfiguration(ctx context.Context, s3Backend backendstore.S3Client, b *v1alpha1.Bucket) (*awss3.PutBucketLifecycleConfigurationOutput, error) { 23 | ctx, span := otel.Tracer("").Start(ctx, "PutBucketLifecycleConfiguration") 24 | defer span.End() 25 | 26 | resp, err := s3Backend.PutBucketLifecycleConfiguration(ctx, GenerateLifecycleConfigurationInput(b.Name, b.Spec.ForProvider.LifecycleConfiguration)) 27 | if err != nil { 28 | err := errors.Wrap(err, errPutLifecycleConfig) 29 | traces.SetAndRecordError(span, err) 30 | 31 | return resp, err 32 | } 33 | 34 | return resp, nil 35 | } 36 | 37 | func DeleteBucketLifecycle(ctx context.Context, s3Backend backendstore.S3Client, bucketName *string) error { 38 | ctx, span := otel.Tracer("").Start(ctx, "DeleteBucketLifecycle") 39 | defer span.End() 40 | 41 | _, err := s3Backend.DeleteBucketLifecycle(ctx, 42 | &awss3.DeleteBucketLifecycleInput{ 43 | Bucket: bucketName, 44 | }, 45 | ) 46 | if err != nil { 47 | err := errors.Wrap(err, errDeleteLifecycle) 48 | traces.SetAndRecordError(span, err) 49 | 50 | return err 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func GetBucketLifecycleConfiguration(ctx context.Context, s3Backend backendstore.S3Client, bucketName *string) (*awss3.GetBucketLifecycleConfigurationOutput, error) { 57 | ctx, span := otel.Tracer("").Start(ctx, "GetBucketLifecycleConfiguration") 58 | defer span.End() 59 | 60 | resp, err := s3Backend.GetBucketLifecycleConfiguration(ctx, &awss3.GetBucketLifecycleConfigurationInput{Bucket: bucketName}) 61 | if resource.IgnoreAny(err, LifecycleConfigurationNotFound, IsBucketNotFound) != nil { 62 | err = errors.Wrap(err, errGetLifecycleConfig) 63 | traces.SetAndRecordError(span, err) 64 | 65 | return resp, err 66 | } 67 | 68 | return resp, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/rgw/object.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "context" 5 | 6 | awss3 "github.com/aws/aws-sdk-go-v2/service/s3" 7 | "github.com/crossplane/crossplane-runtime/pkg/errors" 8 | "github.com/linode/provider-ceph/internal/backendstore" 9 | "github.com/linode/provider-ceph/internal/otel/traces" 10 | "go.opentelemetry.io/otel" 11 | ) 12 | 13 | const ( 14 | errListObjects = "failed to list objects" 15 | errListObjectVersions = "failed to list object versions" 16 | errDeleteObject = "failed to delete object" 17 | errGetObject = "failed to get object" 18 | errPutObject = "failed to put object" 19 | ) 20 | 21 | func GetObject(ctx context.Context, s3Backend backendstore.S3Client, input *awss3.GetObjectInput, o ...func(*awss3.Options)) (*awss3.GetObjectOutput, error) { 22 | ctx, span := otel.Tracer("").Start(ctx, "GetObject") 23 | defer span.End() 24 | 25 | resp, err := s3Backend.GetObject(ctx, input, o...) 26 | if err != nil { 27 | err = errors.Wrap(err, errGetObject) 28 | traces.SetAndRecordError(span, err) 29 | 30 | return resp, err 31 | } 32 | 33 | return resp, nil 34 | } 35 | 36 | func DeleteObject(ctx context.Context, s3Backend backendstore.S3Client, input *awss3.DeleteObjectInput, o ...func(*awss3.Options)) error { 37 | ctx, span := otel.Tracer("").Start(ctx, "DeleteObject") 38 | defer span.End() 39 | 40 | _, err := s3Backend.DeleteObject(ctx, input, o...) 41 | if err != nil { 42 | err = errors.Wrap(err, errDeleteObject) 43 | traces.SetAndRecordError(span, err) 44 | 45 | return err 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func PutObject(ctx context.Context, s3Backend backendstore.S3Client, input *awss3.PutObjectInput, o ...func(*awss3.Options)) error { 52 | ctx, span := otel.Tracer("").Start(ctx, "PutObject") 53 | defer span.End() 54 | 55 | _, err := s3Backend.PutObject(ctx, input, o...) 56 | if err != nil { 57 | err = errors.Wrap(err, errPutObject) 58 | traces.SetAndRecordError(span, err) 59 | 60 | return err 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func ListObjectsV2(ctx context.Context, s3Backend backendstore.S3Client, input *awss3.ListObjectsV2Input, o ...func(*awss3.Options)) (*awss3.ListObjectsV2Output, error) { 67 | ctx, span := otel.Tracer("").Start(ctx, "ListObjectsV2") 68 | defer span.End() 69 | 70 | resp, err := s3Backend.ListObjectsV2(ctx, input, o...) 71 | if err != nil { 72 | err = errors.Wrap(err, errListObjects) 73 | traces.SetAndRecordError(span, err) 74 | 75 | return resp, err 76 | } 77 | 78 | return resp, nil 79 | } 80 | 81 | func ListObjectVersions(ctx context.Context, s3Backend backendstore.S3Client, input *awss3.ListObjectVersionsInput, o ...func(*awss3.Options)) (*awss3.ListObjectVersionsOutput, error) { 82 | ctx, span := otel.Tracer("").Start(ctx, "ListObjectsVersions") 83 | defer span.End() 84 | 85 | resp, err := s3Backend.ListObjectVersions(ctx, input, o...) 86 | if err != nil { 87 | err = errors.Wrap(err, errListObjectVersions) 88 | traces.SetAndRecordError(span, err) 89 | 90 | return resp, err 91 | } 92 | 93 | return resp, nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/rgw/objectlockconfiguration.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "context" 5 | 6 | awss3 "github.com/aws/aws-sdk-go-v2/service/s3" 7 | "github.com/crossplane/crossplane-runtime/pkg/errors" 8 | "github.com/crossplane/crossplane-runtime/pkg/resource" 9 | "github.com/linode/provider-ceph/apis/provider-ceph/v1alpha1" 10 | "github.com/linode/provider-ceph/internal/backendstore" 11 | "github.com/linode/provider-ceph/internal/otel/traces" 12 | "go.opentelemetry.io/otel" 13 | ) 14 | 15 | const ( 16 | errGetObjectLockConfiguration = "failed to get object lock configuration" 17 | errPutObjectLockConfiguration = "failed to put object lock configuration" 18 | ) 19 | 20 | func PutObjectLockConfiguration(ctx context.Context, s3Backend backendstore.S3Client, b *v1alpha1.Bucket) (*awss3.PutObjectLockConfigurationOutput, error) { 21 | ctx, span := otel.Tracer("").Start(ctx, "PutObjectLockConfiguration") 22 | defer span.End() 23 | 24 | resp, err := s3Backend.PutObjectLockConfiguration(ctx, GeneratePutObjectLockConfigurationInput(b.Name, b.Spec.ForProvider.ObjectLockConfiguration)) 25 | if err != nil { 26 | err := errors.Wrap(err, errPutObjectLockConfiguration) 27 | traces.SetAndRecordError(span, err) 28 | 29 | return resp, err 30 | } 31 | 32 | return resp, nil 33 | } 34 | 35 | func GetObjectLockConfiguration(ctx context.Context, s3Backend backendstore.S3Client, bucketName *string) (*awss3.GetObjectLockConfigurationOutput, error) { 36 | ctx, span := otel.Tracer("").Start(ctx, "GetObjectLockConfiguration") 37 | defer span.End() 38 | 39 | resp, err := s3Backend.GetObjectLockConfiguration(ctx, &awss3.GetObjectLockConfigurationInput{Bucket: bucketName}) 40 | if resource.IgnoreAny(err, ObjectLockConfigurationNotFound, IsBucketNotFound) != nil { 41 | err = errors.Wrap(err, errGetObjectLockConfiguration) 42 | traces.SetAndRecordError(span, err) 43 | 44 | return resp, err 45 | } 46 | 47 | return resp, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/rgw/objectlockconfiguration_helpers.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go-v2/aws" 5 | awss3 "github.com/aws/aws-sdk-go-v2/service/s3" 6 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 7 | "github.com/aws/smithy-go" 8 | "github.com/crossplane/crossplane-runtime/pkg/errors" 9 | "github.com/linode/provider-ceph/apis/provider-ceph/v1alpha1" 10 | ) 11 | 12 | // GeneratePutObjectLockConfigurationInput creates the PutObjectLockConfiguration for the AWS SDK 13 | func GeneratePutObjectLockConfigurationInput(name string, config *v1alpha1.ObjectLockConfiguration) *awss3.PutObjectLockConfigurationInput { 14 | return &awss3.PutObjectLockConfigurationInput{ 15 | Bucket: aws.String(name), 16 | ObjectLockConfiguration: GenerateObjectLockConfiguration(config), 17 | } 18 | } 19 | 20 | func GenerateObjectLockConfiguration(inputConfig *v1alpha1.ObjectLockConfiguration) *types.ObjectLockConfiguration { 21 | if inputConfig == nil { 22 | return nil 23 | } 24 | 25 | outputConfig := &types.ObjectLockConfiguration{} 26 | if inputConfig.ObjectLockEnabled != nil { 27 | outputConfig.ObjectLockEnabled = types.ObjectLockEnabled(*inputConfig.ObjectLockEnabled) 28 | } 29 | //nolint:nestif // Multiple checks required 30 | if inputConfig.Rule != nil { 31 | outputConfig.Rule = &types.ObjectLockRule{} 32 | if inputConfig.Rule.DefaultRetention != nil { 33 | outputConfig.Rule.DefaultRetention = &types.DefaultRetention{} 34 | outputConfig.Rule.DefaultRetention.Mode = types.ObjectLockRetentionMode(inputConfig.Rule.DefaultRetention.Mode) 35 | if inputConfig.Rule.DefaultRetention.Days != nil { 36 | outputConfig.Rule.DefaultRetention.Days = inputConfig.Rule.DefaultRetention.Days 37 | } 38 | if inputConfig.Rule.DefaultRetention.Years != nil { 39 | outputConfig.Rule.DefaultRetention.Years = inputConfig.Rule.DefaultRetention.Years 40 | } 41 | } 42 | } 43 | 44 | return outputConfig 45 | } 46 | 47 | // ObjectLockConfigurationNotfoundErrCode is the error code sent by Ceph when the object lock config does not exist 48 | var ObjectLockConfigurationNotFoundErrCode = "ObjectLockConfigurationNotFoundError" 49 | 50 | // ObjectLockConfigurationNotFound is parses the error and validates if the object lock configuration does not exist 51 | func ObjectLockConfigurationNotFound(err error) bool { 52 | var awsErr smithy.APIError 53 | 54 | return errors.As(err, &awsErr) && awsErr.ErrorCode() == ObjectLockConfigurationNotFoundErrCode 55 | } 56 | -------------------------------------------------------------------------------- /internal/rgw/policy.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "context" 5 | 6 | awss3 "github.com/aws/aws-sdk-go-v2/service/s3" 7 | "github.com/crossplane/crossplane-runtime/pkg/errors" 8 | "github.com/linode/provider-ceph/apis/provider-ceph/v1alpha1" 9 | "github.com/linode/provider-ceph/internal/backendstore" 10 | "github.com/linode/provider-ceph/internal/otel/traces" 11 | "go.opentelemetry.io/otel" 12 | ) 13 | 14 | const ( 15 | errGetBucketPolicy = "failed to get bucket policy" 16 | errPutBucketPolicy = "failed to put bucket policy" 17 | errDeleteBucketPolicy = "failed to delete bucket policy" 18 | ) 19 | 20 | func GetBucketPolicy(ctx context.Context, s3Backend backendstore.S3Client, bucketName *string) (*awss3.GetBucketPolicyOutput, error) { 21 | ctx, span := otel.Tracer("").Start(ctx, "GetBucketPolicy") 22 | defer span.End() 23 | 24 | resp, err := s3Backend.GetBucketPolicy(ctx, &awss3.GetBucketPolicyInput{Bucket: bucketName}) 25 | if err != nil { 26 | traces.SetAndRecordError(span, err) 27 | 28 | return resp, errors.Wrap(err, errGetBucketPolicy) 29 | } 30 | 31 | return resp, nil 32 | } 33 | 34 | func PutBucketPolicy(ctx context.Context, s3Backend backendstore.S3Client, b *v1alpha1.Bucket) (*awss3.PutBucketPolicyOutput, error) { 35 | ctx, span := otel.Tracer("").Start(ctx, "PutBucketPolicy") 36 | defer span.End() 37 | 38 | resp, err := s3Backend.PutBucketPolicy(ctx, &awss3.PutBucketPolicyInput{Bucket: &b.Name, Policy: &b.Spec.ForProvider.Policy}) 39 | if err != nil { 40 | traces.SetAndRecordError(span, err) 41 | 42 | return resp, errors.Wrap(err, errPutBucketPolicy) 43 | } 44 | 45 | return resp, nil 46 | } 47 | 48 | func DeleteBucketPolicy(ctx context.Context, s3Backend backendstore.S3Client, bucketName *string) error { 49 | ctx, span := otel.Tracer("").Start(ctx, "DeleteBucketPolicy") 50 | defer span.End() 51 | 52 | _, err := s3Backend.DeleteBucketPolicy(ctx, &awss3.DeleteBucketPolicyInput{Bucket: bucketName}) 53 | if err != nil { 54 | traces.SetAndRecordError(span, err) 55 | 56 | return errors.Wrap(err, errDeleteBucketPolicy) 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/rgw/versioningconfiguration.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "context" 5 | 6 | awss3 "github.com/aws/aws-sdk-go-v2/service/s3" 7 | "github.com/crossplane/crossplane-runtime/pkg/errors" 8 | "github.com/crossplane/crossplane-runtime/pkg/resource" 9 | "github.com/linode/provider-ceph/apis/provider-ceph/v1alpha1" 10 | "github.com/linode/provider-ceph/internal/backendstore" 11 | "github.com/linode/provider-ceph/internal/otel/traces" 12 | "go.opentelemetry.io/otel" 13 | ) 14 | 15 | const ( 16 | errGetBucketVersioning = "failed to get bucket versioning" 17 | errPutBucketVersioning = "failed to put bucket versioning" 18 | ) 19 | 20 | func PutBucketVersioning(ctx context.Context, s3Backend backendstore.S3Client, b *v1alpha1.Bucket) (*awss3.PutBucketVersioningOutput, error) { 21 | ctx, span := otel.Tracer("").Start(ctx, "PutBucketVersioning") 22 | defer span.End() 23 | 24 | resp, err := s3Backend.PutBucketVersioning(ctx, GeneratePutBucketVersioningInput(b.Name, b.Spec.ForProvider.VersioningConfiguration)) 25 | if err != nil { 26 | err := errors.Wrap(err, errPutBucketVersioning) 27 | traces.SetAndRecordError(span, err) 28 | 29 | return resp, err 30 | } 31 | 32 | return resp, nil 33 | } 34 | 35 | func GetBucketVersioning(ctx context.Context, s3Backend backendstore.S3Client, bucketName *string) (*awss3.GetBucketVersioningOutput, error) { 36 | ctx, span := otel.Tracer("").Start(ctx, "GetBucketVersioning") 37 | defer span.End() 38 | 39 | resp, err := s3Backend.GetBucketVersioning(ctx, &awss3.GetBucketVersioningInput{Bucket: bucketName}) 40 | if resource.IgnoreAny(err, IsBucketNotFound) != nil { 41 | err = errors.Wrap(err, errGetBucketVersioning) 42 | traces.SetAndRecordError(span, err) 43 | 44 | return resp, err 45 | } 46 | 47 | return resp, nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/rgw/versioningconfiguration_helpers.go: -------------------------------------------------------------------------------- 1 | package rgw 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go-v2/aws" 5 | awss3 "github.com/aws/aws-sdk-go-v2/service/s3" 6 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 7 | "github.com/linode/provider-ceph/apis/provider-ceph/v1alpha1" 8 | ) 9 | 10 | // GeneratePutBucketVersioningInput creates the PutBucketVersioningInput for the AWS SDK 11 | func GeneratePutBucketVersioningInput(name string, config *v1alpha1.VersioningConfiguration) *awss3.PutBucketVersioningInput { 12 | return &awss3.PutBucketVersioningInput{ 13 | Bucket: aws.String(name), 14 | VersioningConfiguration: GenerateVersioningConfiguration(config), 15 | } 16 | } 17 | 18 | func GenerateVersioningConfiguration(inputConfig *v1alpha1.VersioningConfiguration) *types.VersioningConfiguration { 19 | if inputConfig == nil { 20 | return nil 21 | } 22 | 23 | outputConfig := &types.VersioningConfiguration{} 24 | if inputConfig.MFADelete != nil { 25 | outputConfig.MFADelete = types.MFADelete(*inputConfig.MFADelete) 26 | } 27 | if inputConfig.Status != nil { 28 | outputConfig.Status = types.BucketVersioningStatus(*inputConfig.Status) 29 | } 30 | 31 | return outputConfig 32 | } 33 | -------------------------------------------------------------------------------- /internal/utils/randomstring/randomstring.go: -------------------------------------------------------------------------------- 1 | //go:generate go run -mod=mod github.com/maxbrunsfeld/counterfeiter/v6 -generate 2 | 3 | // Package randomstring provides utilities for generating random strings. 4 | package randomstring 5 | 6 | import ( 7 | "crypto/rand" 8 | "math/big" 9 | 10 | "github.com/crossplane/crossplane-runtime/pkg/errors" 11 | ) 12 | 13 | var ( 14 | errRandomStringLengthTooShort = errors.New("the random string length is too short") 15 | errRandomStringGenerationFailed = errors.New("failed generating random string") 16 | ) 17 | 18 | // Charset is a wrapper for a charset that needs to be provided for random 19 | // string generation. It prevents the need to create a big.Int multiple times. 20 | type Charset struct { 21 | charset string 22 | bigIntLength *big.Int 23 | } 24 | 25 | func NewCharset(charset string) *Charset { 26 | return &Charset{ 27 | charset: charset, 28 | bigIntLength: big.NewInt(int64(len(charset))), 29 | } 30 | } 31 | 32 | //counterfeiter:generate . Generator 33 | type Generator interface { 34 | Generate(prefix string, length int, charset *Charset) (string, error) 35 | } 36 | 37 | type StandardGenerator struct{} 38 | 39 | func (StandardGenerator) Generate(prefix string, length int, charset *Charset) (string, error) { 40 | generatedLength := length - len(prefix) 41 | if generatedLength <= 0 { 42 | return "", errRandomStringLengthTooShort 43 | } 44 | 45 | str := make([]byte, generatedLength) 46 | for i := range str { 47 | j, err := rand.Int(rand.Reader, charset.bigIntLength) 48 | if err != nil { 49 | return "", errors.Wrap(err, errRandomStringGenerationFailed.Error()) 50 | } 51 | str[i] = charset.charset[j.Int64()] 52 | } 53 | 54 | return prefix + string(str), nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/utils/randomstring/randomstring_test.go: -------------------------------------------------------------------------------- 1 | package randomstring 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestStandardGenerator_Generate(t *testing.T) { 10 | t.Parallel() 11 | 12 | tests := map[string]struct { 13 | prefix string 14 | length int 15 | charset *Charset 16 | 17 | assertions func(t *testing.T, str string) 18 | expectedErr error 19 | }{ 20 | "good generation": { 21 | prefix: "AK", 22 | length: 3, 23 | charset: NewCharset("Z"), 24 | assertions: func(t *testing.T, str string) { 25 | t.Helper() 26 | require.Equal(t, "AKZ", str) 27 | }, 28 | }, 29 | "no prefix": { 30 | prefix: "", 31 | length: 3, 32 | charset: NewCharset("Z"), 33 | assertions: func(t *testing.T, str string) { 34 | t.Helper() 35 | require.Equal(t, "ZZZ", str) 36 | }, 37 | }, 38 | "length too short": { 39 | prefix: "AK", 40 | length: 2, 41 | expectedErr: errRandomStringLengthTooShort, 42 | }, 43 | } 44 | 45 | for name, tt := range tests { 46 | t.Run(name, func(t *testing.T) { 47 | t.Parallel() 48 | 49 | str, err := StandardGenerator{}.Generate(tt.prefix, tt.length, tt.charset) 50 | 51 | switch { 52 | case tt.expectedErr != nil: 53 | require.ErrorIs(t, err, tt.expectedErr) 54 | default: 55 | require.NoError(t, err) 56 | tt.assertions(t, str) 57 | } 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /internal/utils/randomstring/randomstringfakes/fake_generator.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package randomstringfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/linode/provider-ceph/internal/utils/randomstring" 8 | ) 9 | 10 | type FakeGenerator struct { 11 | GenerateStub func(string, int, *randomstring.Charset) (string, error) 12 | generateMutex sync.RWMutex 13 | generateArgsForCall []struct { 14 | arg1 string 15 | arg2 int 16 | arg3 *randomstring.Charset 17 | } 18 | generateReturns struct { 19 | result1 string 20 | result2 error 21 | } 22 | generateReturnsOnCall map[int]struct { 23 | result1 string 24 | result2 error 25 | } 26 | invocations map[string][][]interface{} 27 | invocationsMutex sync.RWMutex 28 | } 29 | 30 | func (fake *FakeGenerator) Generate(arg1 string, arg2 int, arg3 *randomstring.Charset) (string, error) { 31 | fake.generateMutex.Lock() 32 | ret, specificReturn := fake.generateReturnsOnCall[len(fake.generateArgsForCall)] 33 | fake.generateArgsForCall = append(fake.generateArgsForCall, struct { 34 | arg1 string 35 | arg2 int 36 | arg3 *randomstring.Charset 37 | }{arg1, arg2, arg3}) 38 | stub := fake.GenerateStub 39 | fakeReturns := fake.generateReturns 40 | fake.recordInvocation("Generate", []interface{}{arg1, arg2, arg3}) 41 | fake.generateMutex.Unlock() 42 | if stub != nil { 43 | return stub(arg1, arg2, arg3) 44 | } 45 | if specificReturn { 46 | return ret.result1, ret.result2 47 | } 48 | return fakeReturns.result1, fakeReturns.result2 49 | } 50 | 51 | func (fake *FakeGenerator) GenerateCallCount() int { 52 | fake.generateMutex.RLock() 53 | defer fake.generateMutex.RUnlock() 54 | return len(fake.generateArgsForCall) 55 | } 56 | 57 | func (fake *FakeGenerator) GenerateCalls(stub func(string, int, *randomstring.Charset) (string, error)) { 58 | fake.generateMutex.Lock() 59 | defer fake.generateMutex.Unlock() 60 | fake.GenerateStub = stub 61 | } 62 | 63 | func (fake *FakeGenerator) GenerateArgsForCall(i int) (string, int, *randomstring.Charset) { 64 | fake.generateMutex.RLock() 65 | defer fake.generateMutex.RUnlock() 66 | argsForCall := fake.generateArgsForCall[i] 67 | return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 68 | } 69 | 70 | func (fake *FakeGenerator) GenerateReturns(result1 string, result2 error) { 71 | fake.generateMutex.Lock() 72 | defer fake.generateMutex.Unlock() 73 | fake.GenerateStub = nil 74 | fake.generateReturns = struct { 75 | result1 string 76 | result2 error 77 | }{result1, result2} 78 | } 79 | 80 | func (fake *FakeGenerator) GenerateReturnsOnCall(i int, result1 string, result2 error) { 81 | fake.generateMutex.Lock() 82 | defer fake.generateMutex.Unlock() 83 | fake.GenerateStub = nil 84 | if fake.generateReturnsOnCall == nil { 85 | fake.generateReturnsOnCall = make(map[int]struct { 86 | result1 string 87 | result2 error 88 | }) 89 | } 90 | fake.generateReturnsOnCall[i] = struct { 91 | result1 string 92 | result2 error 93 | }{result1, result2} 94 | } 95 | 96 | func (fake *FakeGenerator) Invocations() map[string][][]interface{} { 97 | fake.invocationsMutex.RLock() 98 | defer fake.invocationsMutex.RUnlock() 99 | fake.generateMutex.RLock() 100 | defer fake.generateMutex.RUnlock() 101 | copiedInvocations := map[string][][]interface{}{} 102 | for key, value := range fake.invocations { 103 | copiedInvocations[key] = value 104 | } 105 | return copiedInvocations 106 | } 107 | 108 | func (fake *FakeGenerator) recordInvocation(key string, args []interface{}) { 109 | fake.invocationsMutex.Lock() 110 | defer fake.invocationsMutex.Unlock() 111 | if fake.invocations == nil { 112 | fake.invocations = map[string][][]interface{}{} 113 | } 114 | if fake.invocations[key] == nil { 115 | fake.invocations[key] = [][]interface{}{} 116 | } 117 | fake.invocations[key] = append(fake.invocations[key], args) 118 | } 119 | 120 | var _ randomstring.Generator = new(FakeGenerator) 121 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | 6 | commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 7 | "github.com/linode/provider-ceph/apis/provider-ceph/v1alpha1" 8 | apisv1alpha1 "github.com/linode/provider-ceph/apis/v1alpha1" 9 | "k8s.io/utils/strings/slices" 10 | ) 11 | 12 | // MissingStrings returns a slice of all strings that exist 13 | // in sliceA, but not in sliceB. 14 | func MissingStrings(sliceA, sliceB []string) []string { 15 | return slices.Filter(nil, sliceA, func(s string) bool { 16 | return !slices.Contains(sliceB, s) 17 | }) 18 | } 19 | 20 | // MapConditionToHealthStatus takes a crossplane condition and returns the 21 | // corresponding health status, returning Unknown if the condition does not 22 | // map to any health status. 23 | func MapConditionToHealthStatus(condition commonv1.Condition) apisv1alpha1.HealthStatus { 24 | if condition.Equal(v1alpha1.HealthCheckSuccess()) { 25 | return apisv1alpha1.HealthStatusHealthy 26 | } else if condition.Equal(v1alpha1.HealthCheckFail()) { 27 | return apisv1alpha1.HealthStatusUnhealthy 28 | } 29 | 30 | return apisv1alpha1.HealthStatusUnknown 31 | } 32 | 33 | // GetBackendLabel renders label key for provider. 34 | func GetBackendLabel(provider string) string { 35 | return v1alpha1.BackendLabelPrefix + provider 36 | } 37 | 38 | // ResolveHostBase returns the hostbase address with the appropriate http(s) prefix. 39 | func ResolveHostBase(hostBase string, useHTTPS bool) string { 40 | httpsPrefix := "https://" 41 | httpPrefix := "http://" 42 | // Remove prefix in either case if it has been specified. 43 | // Let useHTTPS option take precedence. 44 | hostBase = strings.TrimPrefix(hostBase, httpPrefix) 45 | hostBase = strings.TrimPrefix(hostBase, httpsPrefix) 46 | 47 | if useHTTPS { 48 | return httpsPrefix + hostBase 49 | } 50 | 51 | return httpPrefix + hostBase 52 | } 53 | -------------------------------------------------------------------------------- /internal/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/linode/provider-ceph/apis/provider-ceph/v1alpha1" 9 | apisv1alpha1 "github.com/linode/provider-ceph/apis/v1alpha1" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestMissingStrings(t *testing.T) { 14 | t.Parallel() 15 | cases := map[string]struct { 16 | sliceA []string 17 | sliceB []string 18 | missing []string 19 | }{ 20 | "All strings in sliceA found in sliceB": { 21 | sliceA: []string{ 22 | "cluster-1", 23 | "cluster-2", 24 | }, 25 | sliceB: []string{ 26 | "cluster-1", 27 | "cluster-2", 28 | "cluster-3", 29 | }, 30 | missing: nil, 31 | }, 32 | "All strings in sliceA missing from sliceB": { 33 | sliceA: []string{ 34 | "cluster-1", 35 | "cluster-2", 36 | }, 37 | sliceB: []string{ 38 | "cluster-3", 39 | "cluster-4", 40 | "cluster-5", 41 | }, 42 | missing: []string{ 43 | "cluster-1", 44 | "cluster-2", 45 | }, 46 | }, 47 | "All strings in sliceA missing from empty sliceB": { 48 | sliceA: []string{ 49 | "cluster-1", 50 | "cluster-2", 51 | }, 52 | sliceB: []string{}, 53 | missing: []string{ 54 | "cluster-1", 55 | "cluster-2", 56 | }, 57 | }, 58 | "One string in sliceA is missing from sliceB, others are found": { 59 | sliceA: []string{ 60 | "cluster-1", 61 | "cluster-2", 62 | "cluster-3", 63 | }, 64 | sliceB: []string{ 65 | "cluster-1", 66 | "cluster-2", 67 | "cluster-5", 68 | }, 69 | missing: []string{ 70 | "cluster-3", 71 | }, 72 | }, 73 | } 74 | for name, tc := range cases { 75 | t.Run(name, func(t *testing.T) { 76 | t.Parallel() 77 | missing := MissingStrings(tc.sliceA, tc.sliceB) 78 | if diff := cmp.Diff(tc.missing, missing); diff != "" { 79 | t.Errorf("\n%s\nMissingStrings(...): -want, +got:\n%s\n", name, diff) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestMapConditionToHealthStatus(t *testing.T) { 86 | t.Parallel() 87 | cases := map[string]struct { 88 | c v1.Condition 89 | s apisv1alpha1.HealthStatus 90 | }{ 91 | "HealthCheckSuccess condition": { 92 | c: v1alpha1.HealthCheckSuccess(), 93 | s: apisv1alpha1.HealthStatusHealthy, 94 | }, 95 | "HealthCheckFail condition": { 96 | c: v1alpha1.HealthCheckFail(), 97 | s: apisv1alpha1.HealthStatusUnhealthy, 98 | }, 99 | "HealthCheckDisabled condition": { 100 | c: v1alpha1.HealthCheckDisabled(), 101 | s: apisv1alpha1.HealthStatusUnknown, 102 | }, 103 | "Unavailable condition": { 104 | c: v1.Unavailable(), 105 | s: apisv1alpha1.HealthStatusUnknown, 106 | }, 107 | "Available condition": { 108 | c: v1.Available(), 109 | s: apisv1alpha1.HealthStatusUnknown, 110 | }, 111 | } 112 | for name, tc := range cases { 113 | t.Run(name, func(t *testing.T) { 114 | t.Parallel() 115 | s := MapConditionToHealthStatus(tc.c) 116 | assert.Equal(t, s, tc.s) 117 | }) 118 | } 119 | } 120 | 121 | func TestResolveHostBase(t *testing.T) { 122 | t.Parallel() 123 | 124 | type args struct { 125 | hostBase string 126 | useHTTPS bool 127 | } 128 | 129 | cases := map[string]struct { 130 | args args 131 | want string 132 | }{ 133 | "Use https without prefix": { 134 | args: args{ 135 | hostBase: "localhost", 136 | useHTTPS: true, 137 | }, 138 | want: "https://localhost", 139 | }, 140 | "Use http without prefix": { 141 | args: args{ 142 | hostBase: "localhost", 143 | useHTTPS: false, 144 | }, 145 | want: "http://localhost", 146 | }, 147 | "Use https with prefix": { 148 | args: args{ 149 | hostBase: "http://localhost", 150 | useHTTPS: true, 151 | }, 152 | want: "https://localhost", 153 | }, 154 | "Use http with prefix": { 155 | args: args{ 156 | hostBase: "http://localhost", 157 | useHTTPS: false, 158 | }, 159 | want: "http://localhost", 160 | }, 161 | } 162 | for name, tc := range cases { 163 | t.Run(name, func(t *testing.T) { 164 | t.Parallel() 165 | 166 | got := ResolveHostBase(tc.args.hostBase, tc.args.useHTTPS) 167 | if diff := cmp.Diff(tc.want, got); diff != "" { 168 | t.Errorf("\n%s\nresolveHostBase(...): -want, +got:\n%s\n", tc.want, diff) 169 | } 170 | }) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /package/crossplane.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: meta.pkg.crossplane.io/v1alpha1 2 | kind: Provider 3 | metadata: 4 | name: provider-ceph 5 | annotations: 6 | meta.crossplane.io/maintainer: github.com/linode/provider-ceph 7 | meta.crossplane.io/source: github.com/linode/provider-ceph 8 | meta.crossplane.io/description: | 9 | A Crossplane provider that performs CRUD operations on s3 buckets. 10 | meta.crossplane.io/readme: | 11 | A Crossplane provider that performs CRUD operations on s3 buckets. 12 | friendly-name.meta.crossplane.io: Provider Ceph 13 | spec: 14 | controller: 15 | image: xpkg.upbound.io/linode/provider-ceph:v1.0.5-rc.0.3.g72eb3c2 16 | -------------------------------------------------------------------------------- /package/webhookconfigurations/manifests.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: ValidatingWebhookConfiguration 3 | metadata: 4 | name: validating-webhook-configuration 5 | webhooks: 6 | - admissionReviewVersions: 7 | - v1 8 | clientConfig: 9 | service: 10 | name: provider-ceph 11 | namespace: crossplane-system 12 | path: /validate-provider-ceph-ceph-crossplane-io-v1alpha1-bucket 13 | port: 9443 14 | failurePolicy: Fail 15 | name: bucket-validation.providerceph.crossplane.io 16 | objectSelector: 17 | matchLabels: 18 | provider-ceph.crossplane.io/validation-required: "true" 19 | rules: 20 | - apiGroups: 21 | - provider-ceph.ceph.crossplane.io 22 | apiVersions: 23 | - v1alpha1 24 | operations: 25 | - CREATE 26 | - UPDATE 27 | resources: 28 | - buckets 29 | sideEffects: None 30 | -------------------------------------------------------------------------------- /staging/validatingwebhookconfiguration/.gitignore: -------------------------------------------------------------------------------- 1 | manifests.yaml 2 | -------------------------------------------------------------------------------- /staging/validatingwebhookconfiguration/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | 4 | patches: 5 | - path: object-selector-patch.yaml 6 | target: 7 | kind: ValidatingWebhookConfiguration 8 | - path: service-patch.yaml 9 | target: 10 | kind: ValidatingWebhookConfiguration 11 | -------------------------------------------------------------------------------- /staging/validatingwebhookconfiguration/object-selector-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: ValidatingWebhookConfiguration 3 | metadata: 4 | name: validating-webhook-configuration 5 | webhooks: 6 | - name: bucket-validation.providerceph.crossplane.io 7 | objectSelector: 8 | matchLabels: 9 | provider-ceph.crossplane.io/validation-required: "true" 10 | -------------------------------------------------------------------------------- /staging/validatingwebhookconfiguration/service-patch-cert-manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: ValidatingWebhookConfiguration 3 | metadata: 4 | name: validating-webhook-configuration 5 | annotations: 6 | cert-manager.io/inject-ca-from: crossplane-system/crossplane-provider-provider-ceph 7 | webhooks: 8 | - name: bucket-validation.providerceph.crossplane.io 9 | clientConfig: 10 | caBundle: Cg== 11 | service: 12 | name: provider-ceph 13 | namespace: crossplane-system 14 | port: 9443 15 | -------------------------------------------------------------------------------- /staging/validatingwebhookconfiguration/service-patch-dev.tpl.yaml: -------------------------------------------------------------------------------- 1 | - op: remove 2 | path: /webhooks/0/clientConfig/service 3 | - op: add 4 | path: /webhooks/0/clientConfig/url 5 | value: https://#WEBHOOK_HOST#/validate-provider-ceph-ceph-crossplane-io-v1alpha1-bucket 6 | -------------------------------------------------------------------------------- /staging/validatingwebhookconfiguration/service-patch-stock.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: ValidatingWebhookConfiguration 3 | metadata: 4 | name: validating-webhook-configuration 5 | webhooks: 6 | - name: bucket-validation.providerceph.crossplane.io 7 | clientConfig: 8 | service: 9 | name: provider-ceph 10 | namespace: crossplane-system 11 | port: 9443 12 | -------------------------------------------------------------------------------- /staging/validatingwebhookconfiguration/service-patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: ValidatingWebhookConfiguration 3 | metadata: 4 | name: validating-webhook-configuration 5 | webhooks: 6 | - name: bucket-validation.providerceph.crossplane.io 7 | clientConfig: 8 | service: 9 | name: provider-ceph 10 | namespace: crossplane-system 11 | port: 9443 12 | --------------------------------------------------------------------------------