diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 00000000..224b69c6 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,28 @@ +name: Backport + +on: + # NOTE(negz): This is a risky target, but we run this action only when and if + # a PR is closed, then filter down to specifically merged PRs. We also don't + # invoke any scripts, etc from within the repo. I believe the fact that we'll + # be able to review PRs before this runs makes this fairly safe. + # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ + pull_request_target: + types: [closed] + # See also commands.yml for the /backport triggered variant of this workflow. + +jobs: + # NOTE(negz): I tested many backport GitHub actions before landing on this + # one. Many do not support merge commits, or do not support pull requests with + # more than one commit. This one does. It also handily links backport PRs with + # new PRs, and provides commentary and instructions when it can't backport. + # The main gotcha with this action is that PRs _must_ be labelled before they're + # merged to trigger a backport. + open-pr: + runs-on: ubuntu-24.04 + if: github.event.pull_request.merged + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Open Backport PR + uses: korthout/backport-action@bd68141f079bd036e45ea8149bc9d174d5a04703 # v1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 271413a1..67f7cb52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,9 @@ on: env: # Common versions - GO_VERSION: '1.21' - GOLANGCI_VERSION: 'v1.54.0' - DOCKER_BUILDX_VERSION: 'v0.8.2' + GO_VERSION: '1.23' + GOLANGCI_VERSION: 'v2.1.2' + DOCKER_BUILDX_VERSION: 'v0.23.0' # Common users. We can't run a step 'if secrets.AWS_USR != ""' but we can run # a step 'if env.AWS_USR' != ""', so we copy these to succinctly test whether @@ -23,13 +23,13 @@ env: jobs: detect-noop: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 outputs: noop: ${{ steps.noop.outputs.should_skip }} steps: - name: Detect No-op Changes id: noop - uses: fkirc/skip-duplicate-actions@v2.1.0 + uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1 with: github_token: ${{ secrets.GITHUB_TOKEN }} paths_ignore: '["**.md", "**.png", "**.jpg"]' @@ -38,99 +38,104 @@ jobs: lint: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: detect-noop if: needs.detect-noop.outputs.noop != 'true' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: true - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: ${{ env.GO_VERSION }} - name: Find the Go Build Cache id: go - run: echo "::set-output name=cache::$(make go.cachedir)" + run: echo "cachedir=$(make go.cachedir)" >> $GITHUB_ENV - name: Cache the Go Build Cache - uses: actions/cache@v2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: - path: ${{ steps.go.outputs.cache }} + path: ${{ env.cachedir }} key: ${{ runner.os }}-build-lint-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-build-lint- - name: Cache Go Dependencies - uses: actions/cache@v2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: .work/pkg key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-pkg- - - name: Vendor Dependencies - run: make vendor vendor.check + - name: Download Go Modules + run: make modules.download modules.check - # We could run 'make lint' to ensure our desired Go version, but we prefer - # this action because it leaves 'annotations' (i.e. it comments on PRs to - # point out linter violations). + # We could run 'make lint' but we prefer this action because it leaves + # 'annotations' (i.e. it comments on PRs to point out linter violations). - name: Lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 with: version: ${{ env.GOLANGCI_VERSION }} - skip-go-installation: true check-diff: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: detect-noop if: needs.detect-noop.outputs.noop != 'true' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: true - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: ${{ env.GO_VERSION }} - name: Find the Go Build Cache id: go - run: echo "::set-output name=cache::$(make go.cachedir)" + run: echo "cachedir=$(make go.cachedir)" >> $GITHUB_ENV - name: Cache the Go Build Cache - uses: actions/cache@v2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: - path: ${{ steps.go.outputs.cache }} + path: ${{ env.cachedir }} key: ${{ runner.os }}-build-check-diff-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-build-check-diff- - name: Cache Go Dependencies - uses: actions/cache@v2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: .work/pkg key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-pkg- - - name: Vendor Dependencies - run: make vendor vendor.check + - name: Download Go Modules + run: make modules.download modules.check - name: Check Diff - run: make check-diff + id: check-diff + run: | + mkdir _output + make check-diff + + - name: Show diff + if: failure() && steps.check-diff.outcome == 'failure' + run: git diff unit-tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: detect-noop if: needs.detect-noop.outputs.noop != 'true' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: true @@ -138,59 +143,59 @@ jobs: run: git fetch --prune --unshallow - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: ${{ env.GO_VERSION }} - name: Find the Go Build Cache id: go - run: echo "::set-output name=cache::$(make go.cachedir)" + run: echo "cachedir=$(make go.cachedir)" >> $GITHUB_ENV - name: Cache the Go Build Cache - uses: actions/cache@v2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: - path: ${{ steps.go.outputs.cache }} + path: ${{ env.cachedir }} key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-build-unit-tests- - name: Cache Go Dependencies - uses: actions/cache@v2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: .work/pkg key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-pkg- - - name: Vendor Dependencies - run: make vendor vendor.check + - name: Download Go Modules + run: make modules.download modules.check - name: Run Unit Tests run: make -j2 test - name: Publish Unit Test Coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 with: flags: unittests - file: _output/tests/linux_amd64/coverage.txt + files: _output/tests/linux_amd64/coverage.txt e2e-tests: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: detect-noop if: needs.detect-noop.outputs.noop != 'true' steps: - name: Setup QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 with: platforms: all - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 with: version: ${{ env.DOCKER_BUILDX_VERSION }} install: true - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: true @@ -198,61 +203,68 @@ jobs: run: git fetch --prune --unshallow - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: ${{ env.GO_VERSION }} - name: Find the Go Build Cache id: go - run: echo "::set-output name=cache::$(make go.cachedir)" + run: echo "cachedir=$(make go.cachedir)" >> $GITHUB_ENV - name: Cache the Go Build Cache - uses: actions/cache@v2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: - path: ${{ steps.go.outputs.cache }} - key: ${{ runner.os }}-build-e2e-tests-${{ hashFiles('**/go.sum') }} - restore-keys: ${{ runner.os }}-build-e2e-tests- + path: ${{ env.cachedir }} + key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }} + restore-keys: ${{ runner.os }}-build-unit-tests- - name: Cache Go Dependencies - uses: actions/cache@v2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: .work/pkg key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-pkg- + restore-keys: ${{ runner.os }}-pkg- - - name: Vendor Dependencies - run: make vendor vendor.check + - name: Download Go Modules + run: make modules.download modules.check - name: Build Helm Chart run: make -j2 build - env: - # We're using docker buildx, which doesn't actually load the images it - # builds by default. Specifying --load does so. - BUILD_ARGS: "--load" + #env: + # # We're using docker buildx, which doesn't actually load the images it + # # builds by default. Specifying --load does so. + # BUILD_ARGS: "--load" - name: Run E2E Tests - run: make e2e USE_HELM3=true + run: make e2e USE_HELM=true publish-artifacts: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: detect-noop if: needs.detect-noop.outputs.noop != 'true' steps: - name: Setup QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 with: platforms: all - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 with: version: ${{ env.DOCKER_BUILDX_VERSION }} install: true + - name: Login to Upbound + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + if: env.XPKG_ACCESS_ID != '' + with: + registry: xpkg.upbound.io + username: ${{ secrets.XPKG_ACCESS_ID }} + password: ${{ secrets.XPKG_TOKEN }} + - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: true @@ -260,30 +272,30 @@ jobs: run: git fetch --prune --unshallow - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 with: go-version: ${{ env.GO_VERSION }} - name: Find the Go Build Cache id: go - run: echo "::set-output name=cache::$(make go.cachedir)" + run: echo "cachedir=$(make go.cachedir)" >> $GITHUB_ENV - name: Cache the Go Build Cache - uses: actions/cache@v2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: - path: ${{ steps.go.outputs.cache }} + path: ${{ env.cachedir }} key: ${{ runner.os }}-build-publish-artifacts-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-build-publish-artifacts- - name: Cache Go Dependencies - uses: actions/cache@v2 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: .work/pkg key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-pkg- - - name: Vendor Dependencies - run: make vendor vendor.check + - name: Download Go Modules + run: make modules.download modules.check - name: Build Artifacts run: make -j2 build.all @@ -293,26 +305,22 @@ jobs: BUILD_ARGS: "--load" - name: Publish Artifacts to GitHub - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: output path: _output/** + - name: Publish Artifacts + if: env.XPKG_ACCESS_ID != '' + run: make publish BRANCH_NAME=${GITHUB_REF##*/} + - name: Login to Docker - uses: docker/login-action@v1 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 if: env.CONTRIB_DOCKER_USR != '' with: username: ${{ secrets.CONTRIB_DOCKER_USR }} password: ${{ secrets.CONTRIB_DOCKER_PSW }} - - name: Login to Upbound - uses: docker/login-action@v1 - if: env.XPKG_ACCESS_ID != '' - with: - registry: xpkg.upbound.io - username: ${{ secrets.XPKG_ACCESS_ID }} - password: ${{ secrets.XPKG_TOKEN }} - - name: Publish Artifacts to S3 and Docker Hub run: make -j2 publish BRANCH_NAME=${GITHUB_REF##*/} if: env.AWS_USR != '' && env.CONTRIB_DOCKER_USR != '' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..e4071a1f --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,42 @@ +name: CodeQL + +on: + push: + branches: + - master + - release-* + workflow_dispatch: {} + +jobs: + detect-noop: + runs-on: ubuntu-24.04 + outputs: + noop: ${{ steps.noop.outputs.should_skip }} + steps: + - name: Detect No-op Changes + id: noop + uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + paths_ignore: '["**.md", "**.png", "**.jpg"]' + do_not_skip: '["workflow_dispatch", "schedule", "push"]' + concurrent_skipping: false + + analyze: + runs-on: ubuntu-24.04 + needs: detect-noop + if: needs.detect-noop.outputs.noop != 'true' + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: true + + - name: Initialize CodeQL + uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 + with: + languages: go + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml new file mode 100644 index 00000000..5de09f44 --- /dev/null +++ b/.github/workflows/commands.yml @@ -0,0 +1,86 @@ +name: Comment Commands + +on: issue_comment + +jobs: + points: + runs-on: ubuntu-24.04 + if: startsWith(github.event.comment.body, '/points') + + steps: + - name: Extract Command + id: command + uses: xt0rted/slash-command-action@bf51f8f5f4ea3d58abc7eca58f77104182b23e88 # v2.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + command: points + reaction: "true" + reaction-type: "eyes" + allow-edits: "false" + permission-level: write + - name: Handle Command + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + env: + POINTS: ${{ steps.command.outputs.command-arguments }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const points = process.env.POINTS + + if (isNaN(parseInt(points))) { + console.log("Malformed command - expected '/points '") + github.reactions.createForIssueComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + content: "confused" + }) + return + } + const label = "points/" + points + + // Delete our needs-points-label label. + try { + await github.issues.deleteLabel({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + name: ['needs-points-label'] + }) + console.log("Deleted 'needs-points-label' label.") + } + catch(e) { + console.log("Label 'needs-points-label' probably didn't exist.") + } + + // Add our points label. + github.issues.addLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: [label] + }) + console.log("Added '" + label + "' label.") + + # NOTE(negz): See also backport.yml, which is the variant that triggers on PR + # merge rather than on comment. + backport: + runs-on: ubuntu-24.04 + if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/backport') + steps: + - name: Extract Command + id: command + uses: xt0rted/slash-command-action@865ee04a1dfc8aa2571513eee8e84b5377153511 # v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + command: backport + reaction: "true" + reaction-type: "eyes" + allow-edits: "false" + permission-level: write + + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Open Backport PR + uses: korthout/backport-action@bd68141f079bd036e45ea8149bc9d174d5a04703 # v1 diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml index e297e8ed..eef81b9a 100644 --- a/.github/workflows/promote.yml +++ b/.github/workflows/promote.yml @@ -9,12 +9,9 @@ on: channel: description: 'Release channel' required: true - default: 'alpha' + default: 'stable' env: - # Common versions - GO_VERSION: '1.18' - # Common users. We can't run a step 'if secrets.AWS_USR != ""' but we can run # a step 'if env.AWS_USR' != ""', so we copy these to succinctly test whether # credentials have been provided before trying to run steps that need them. @@ -24,11 +21,11 @@ env: jobs: promote-artifacts: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: submodules: true @@ -36,14 +33,14 @@ jobs: run: git fetch --prune --unshallow - name: Login to Docker - uses: docker/login-action@v1 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 if: env.CONTRIB_DOCKER_USR != '' with: username: ${{ secrets.CONTRIB_DOCKER_USR }} password: ${{ secrets.CONTRIB_DOCKER_PSW }} - name: Login to Upbound - uses: docker/login-action@v1 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 if: env.XPKG_ACCESS_ID != '' with: registry: xpkg.upbound.io diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index 3b272eaf..1424b6fb 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -12,15 +12,15 @@ on: jobs: create-tag: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Create Tag - uses: negz/create-tag@v1 + uses: negz/create-tag@39bae1e0932567a58c20dea5a1a0d18358503320 # v1 with: version: ${{ github.event.inputs.version }} message: ${{ github.event.inputs.message }} - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitmodules b/.gitmodules index c2fad470..8f84209c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "build"] path = build - url = https://github.com/upbound/build + url = https://github.com/crossplane/build diff --git a/.golangci.yml b/.golangci.yml index 771bc4fa..5b6c12b7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,185 +1,165 @@ +version: "2" run: - deadline: 2m - - skip-files: - - "zz_generated\\..+\\.go$" - -output: - # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" - format: colored-line-number - -linters-settings: - errcheck: - # report about not checking of errors in type assetions: `a := b.(MyStruct)`; - # default is false: such cases aren't reported by default. - check-type-assertions: false - - # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; - # default is false: such cases aren't reported by default. - check-blank: false - - # [deprecated] comma-separated list of pairs of the form pkg:regex - # the regex is used to ignore names within pkg. (default "fmt:.*"). - # see https://github.com/kisielk/errcheck#the-deprecated-method for details - ignore: fmt:.*,io/ioutil:^Read.* - - govet: - # report about shadowed variables - check-shadowing: false - - golint: - # minimal confidence for issues, default is 0.8 - min-confidence: 0.8 - - gofmt: - # simplify code: gofmt with `-s` option, true by default - simplify: true - - goimports: - # put imports beginning with prefix after 3rd-party packages; - # it's a comma-separated list of prefixes - local-prefixes: github.com/crossplane/provider-sql - - gocyclo: - # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 10 - - maligned: - # print struct with more effective memory layout or not, false by default - suggest-new: true - - dupl: - # tokens count to trigger issue, 150 by default - threshold: 100 - - goconst: - # minimal length of string constant, 3 by default - min-len: 3 - # minimal occurrences count to trigger, 3 by default - min-occurrences: 5 - - lll: - # tab width in spaces. Default to 1. - tab-width: 1 - - unused: - # treat code as a program (not a library) and report unused exported identifiers; default is false. - # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find funcs usages. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - check-exported: false - - unparam: - # Inspect exported functions, default is false. Set to true if no external program/library imports your code. - # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find external interfaces. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - check-exported: false - - nakedret: - # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 - max-func-lines: 30 - - prealloc: - # XXX: we don't recommend using this linter before doing performance profiling. - # For most programs usage of prealloc will be a premature optimization. - - # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. - # True by default. - simple: true - range-loops: true # Report preallocation suggestions on range loops, true by default - for-loops: false # Report preallocation suggestions on for loops, false by default - - gocritic: - # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks. - # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". - enabled-tags: - - performance - - settings: # settings passed to gocritic - captLocal: # must be valid enabled check name - paramsOnly: true - rangeValCopy: - sizeThreshold: 32 + timeout: 2m + +formatters: + # Enable specific formatter. + # Default: [] (uses standard Go formatting) + enable: + - gofmt + - goimports + # - gci + # - gofumpt + # - golines + + settings: + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + + goimports: + # put imports beginning with prefix after 3rd-party packages + local-prefixes: + - github.com/crossplane/provider-sql linters: + default: standard enable: - - megacheck - govet - gocyclo - gocritic - - interfacer + - iface - goconst - - goimports - - gofmt # We enable this as well as goimports for its simplify mode. - prealloc - - golint + - revive - unconvert - misspell - nakedret - - presets: - - bugs - - unused - fast: false - - + settings: + errcheck: + # report about not checking of errors in type assetions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: false + + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: false + + exclude-functions: + - fmt:.* + - io/ioutil:^Read.* + + govet: + # report about shadowed variables + disable: + - shadow + + revive: + # minimal confidence for issues, default is 0.8 + confidence: 0.8 + + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 10 + + dupl: + # tokens count to trigger issue, 150 by default + threshold: 100 + + goconst: + # minimal length of string constant, 3 by default + min-len: 3 + # minimal occurrences count to trigger, 3 by default + min-occurrences: 5 + + lll: + # tab width in spaces. Default to 1. + tab-width: 1 + + unused: + exported-fields-are-used: false + + unparam: + # Inspect exported functions, default is false. Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it's called for subdir of a project it can't find external interfaces. All text editor integrations + # with golangci-lint call it on a directory with the changed file. + check-exported: false + + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 30 + + prealloc: + # XXX: we don't recommend using this linter before doing performance profiling. + # For most programs usage of prealloc will be a premature optimization. + + # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. + # True by default. + simple: true + range-loops: true # Report preallocation suggestions on range loops, true by default + for-loops: false # Report preallocation suggestions on for loops, false by default + + gocritic: + # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks. + # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". + enabled-tags: + - performance + + settings: # settings passed to gocritic + captLocal: # must be valid enabled check name + paramsOnly: true + rangeValCopy: + sizeThreshold: 32 + exclusions: + rules: + # Exclude some linters from running on tests files. + - path: _test(ing)?\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + - scopelint + - unparam + + # Ease some gocritic warnings on test files. + - path: _test\.go + text: "(unnamedResult|exitAfterDefer)" + linters: + - gocritic + + # These are performance optimisations rather than style issues per se. + # They warn when function arguments or range values copy a lot of memory + # rather than using a pointer. + - text: "(hugeParam|rangeValCopy):" + linters: + - gocritic + + # This "TestMain should call os.Exit to set exit code" warning is not clever + # enough to notice that we call a helper method that calls os.Exit. + - text: "SA3000:" + linters: + - staticcheck + + - text: "k8s.io/api/core/v1" + linters: + - goimports + + # This is a "potential hardcoded credentials" warning. It's triggered by + # any variable with 'secret' in the same, and thus hits a lot of false + # positives in Kubernetes land where a Secret is an object type. + - text: "G101:" + linters: + - gosec + - gas + + # This is an 'errors unhandled' warning that duplicates errcheck. + - text: "G104:" + linters: + - gosec + - gas issues: - # Excluding configuration per-path and per-linter - exclude-rules: - # Exclude some linters from running on tests files. - - path: _test(ing)?\.go - linters: - - gocyclo - - errcheck - - dupl - - gosec - - scopelint - - unparam - - # Ease some gocritic warnings on test files. - - path: _test\.go - text: "(unnamedResult|exitAfterDefer)" - linters: - - gocritic - - # These are performance optimisations rather than style issues per se. - # They warn when function arguments or range values copy a lot of memory - # rather than using a pointer. - - text: "(hugeParam|rangeValCopy):" - linters: - - gocritic - - # This "TestMain should call os.Exit to set exit code" warning is not clever - # enough to notice that we call a helper method that calls os.Exit. - - text: "SA3000:" - linters: - - staticcheck - - - text: "k8s.io/api/core/v1" - linters: - - goimports - - # This is a "potential hardcoded credentials" warning. It's triggered by - # any variable with 'secret' in the same, and thus hits a lot of false - # positives in Kubernetes land where a Secret is an object type. - - text: "G101:" - linters: - - gosec - - gas - - # This is an 'errors unhandled' warning that duplicates errcheck. - - text: "G104:" - linters: - - gosec - - gas - - # Independently from option `exclude` we use default exclude patterns, - # it can be disabled by this option. To list all - # excluded by default patterns execute `golangci-lint run --help`. - # Default value for this option is true. - exclude-use-default: false - # Show only new issues: if there are unstaged changes or untracked files, # only those changes are analyzed, else only changes in HEAD~ are analyzed. # It's a super-useful option for integration of golangci-lint into existing @@ -189,7 +169,7 @@ issues: new: false # Maximum issues count per one linter. Set to 0 to disable. Default is 50. - max-per-linter: 0 + max-issues-per-linter: 0 # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. max-same-issues: 0 diff --git a/Makefile b/Makefile index 6b532fb2..715600c7 100644 --- a/Makefile +++ b/Makefile @@ -23,21 +23,19 @@ NPROCS ?= 1 # to half the number of CPU cores. GO_TEST_PARALLEL := $(shell echo $$(( $(NPROCS) / 2 ))) +GOLANGCILINT_VERSION ?= 2.1.2 GO_STATIC_PACKAGES = $(GO_PROJECT)/cmd/provider GO_LDFLAGS += -X $(GO_PROJECT)/pkg/version.Version=$(VERSION) GO_SUBDIRS += cmd pkg apis GO111MODULE = on -include build/makelib/golang.mk -# kind-related versions -KIND_VERSION ?= v0.12.0 -KIND_NODE_IMAGE_TAG ?= v1.23.4 - # ==================================================================================== # Setup Kubernetes tools - -UP_VERSION = v0.31.0 -UP_CHANNEL = stable +KIND_NODE_IMAGE_TAG ?= v1.30.13 +KIND_VERSION ?= v0.29.0 +KUBECTL_VERSION ?= v1.30.13 +CROSSPLANE_CLI_VERSION ?= v1.20.0 -include build/makelib/k8s_tools.mk # ==================================================================================== @@ -46,7 +44,6 @@ UP_CHANNEL = stable IMAGES = provider-sql -include build/makelib/imagelight.mk - # ==================================================================================== # Setup XPKG @@ -87,7 +84,7 @@ generate: crds.clean e2e.run: test-integration # Run integration tests. -test-integration: $(KIND) $(KUBECTL) $(UP) $(HELM3) +test-integration: $(KIND) $(KUBECTL) $(UP) $(HELM) @$(INFO) running integration tests using kind $(KIND_VERSION) @KIND_NODE_IMAGE_TAG=${KIND_NODE_IMAGE_TAG} $(ROOT_DIR)/cluster/local/integration_tests.sh || $(FAIL) @$(OK) integration tests passed @@ -124,7 +121,7 @@ dev: $(KIND) $(KUBECTL) @$(KIND) create cluster --name=$(PROJECT_NAME)-dev @$(KUBECTL) cluster-info --context kind-$(PROJECT_NAME)-dev @$(INFO) Installing Crossplane CRDs - @$(KUBECTL) apply -k https://github.com/crossplane/crossplane//cluster?ref=master + @$(KUBECTL) apply --server-side -k https://github.com/crossplane/crossplane//cluster?ref=master @$(INFO) Installing Provider SQL CRDs @$(KUBECTL) apply -R -f package/crds @$(INFO) Starting Provider SQL controllers diff --git a/OWNERS.md b/OWNERS.md index 11ab618c..9668c5f9 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -10,13 +10,14 @@ Please see [GOVERNANCE.md] for governance guidelines and responsibilities. * Jeroen Op 't Eynde ([Duologic](https://github.com/Duologic)) * James Wilson ([jdotw](https://github.com/jdotw)) -* Nic Cope ([negz](https://github.com/negz)) * Javier Palomo ([jvrplmlmn](https://github.com/jvrplmlmn)) * Iain Lane ([iainlane](https://github.com/iainlane)) +* Carl Henrik Lunde ([chlunde](https://github.com/chlunde)) ## Emeritus maintainers * Ben Agricola ([benagricola](https://github.com/benagricola)) * Simon Ruegg ([srueg](https://github.com/srueg)) +* Nic Cope ([negz](https://github.com/negz)) -[GOVERNANCE.md]: https://github.com/crossplane/crossplane/blob/master/GOVERNANCE.md \ No newline at end of file +[GOVERNANCE.md]: https://github.com/crossplane/crossplane/blob/master/GOVERNANCE.md diff --git a/apis/mssql/v1alpha1/grant_types.go b/apis/mssql/v1alpha1/grant_types.go index 7e2aa4e5..d717c2ae 100644 --- a/apis/mssql/v1alpha1/grant_types.go +++ b/apis/mssql/v1alpha1/grant_types.go @@ -55,6 +55,11 @@ type GrantParameters struct { // for available privileges. Permissions GrantPermissions `json:"permissions"` + // Schema for the permissions to be granted for. + // +immutable + // +optional + Schema *string `json:"schema,omitempty"` + // User this grant is for. // +optional // +crossplane:generate:reference:type=User @@ -100,6 +105,7 @@ type GrantStatus struct { // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" // +kubebuilder:printcolumn:name="ROLE",type="string",JSONPath=".spec.forProvider.user" // +kubebuilder:printcolumn:name="DATABASE",type="string",JSONPath=".spec.forProvider.database" +// +kubebuilder:printcolumn:name="SCHEMA",type="string",JSONPath=".spec.forProvider.schema" // +kubebuilder:printcolumn:name="PERMISSIONS",type="string",JSONPath=".spec.forProvider.permissions" // +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,sql} type Grant struct { diff --git a/apis/mssql/v1alpha1/user_types.go b/apis/mssql/v1alpha1/user_types.go index 846f16cd..eb74775e 100644 --- a/apis/mssql/v1alpha1/user_types.go +++ b/apis/mssql/v1alpha1/user_types.go @@ -36,18 +36,26 @@ type UserStatus struct { // UserParameters define the desired state of a MSSQL user instance. type UserParameters struct { + // Database allows you to specify the name of the Database the USER is created for. // +crossplane:generate:reference:type=Database Database *string `json:"database,omitempty"` - // DatabaseRef allows you to specify custom resource name of the Database + // DatabaseRef allows you to specify custom resource name of the Database the USER is created for. // to fill Database field. DatabaseRef *xpv1.Reference `json:"databaseRef,omitempty"` - // DatabaseSelector allows you to use selector constraints to select a - // Database. + // DatabaseSelector allows you to use selector constraints to select a Database the USER is created for. DatabaseSelector *xpv1.Selector `json:"databaseSelector,omitempty"` // PasswordSecretRef references the secret that contains the password used // for this user. If no reference is given, a password will be auto-generated. // +optional PasswordSecretRef *xpv1.SecretKeySelector `json:"passwordSecretRef,omitempty"` + // LoginDatabase allows you to specify the name of the Database to be used to create the user LOGIN in (normally master). + // +crossplane:generate:reference:type=Database + LoginDatabase *string `json:"loginDatabase,omitempty"` + // DatabaseRef allows you to specify custom resource name of the Database to be used to create the user LOGIN in (normally master). + // to fill Database field. + LoginDatabaseRef *xpv1.Reference `json:"loginDatabaseRef,omitempty"` + // DatabaseSelector allows you to use selector constraints to select a Database to be used to create the user LOGIN in (normally master). + LoginDatabaseSelector *xpv1.Selector `json:"loginDatabaseSelector,omitempty"` } // A UserObservation represents the observed state of a MSSQL user. diff --git a/apis/mssql/v1alpha1/zz_generated.deepcopy.go b/apis/mssql/v1alpha1/zz_generated.deepcopy.go index 729bad36..bcaee39a 100644 --- a/apis/mssql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/mssql/v1alpha1/zz_generated.deepcopy.go @@ -183,6 +183,11 @@ func (in *GrantParameters) DeepCopyInto(out *GrantParameters) { *out = make(GrantPermissions, len(*in)) copy(*out, *in) } + if in.Schema != nil { + in, out := &in.Schema, &out.Schema + *out = new(string) + **out = **in + } if in.User != nil { in, out := &in.User, &out.User *out = new(string) @@ -543,6 +548,21 @@ func (in *UserParameters) DeepCopyInto(out *UserParameters) { *out = new(v1.SecretKeySelector) **out = **in } + if in.LoginDatabase != nil { + in, out := &in.LoginDatabase, &out.LoginDatabase + *out = new(string) + **out = **in + } + if in.LoginDatabaseRef != nil { + in, out := &in.LoginDatabaseRef, &out.LoginDatabaseRef + *out = new(v1.Reference) + (*in).DeepCopyInto(*out) + } + if in.LoginDatabaseSelector != nil { + in, out := &in.LoginDatabaseSelector, &out.LoginDatabaseSelector + *out = new(v1.Selector) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserParameters. diff --git a/apis/mssql/v1alpha1/zz_generated.resolvers.go b/apis/mssql/v1alpha1/zz_generated.resolvers.go index 8c077a66..1be14579 100644 --- a/apis/mssql/v1alpha1/zz_generated.resolvers.go +++ b/apis/mssql/v1alpha1/zz_generated.resolvers.go @@ -89,5 +89,21 @@ func (mg *User) ResolveReferences(ctx context.Context, c client.Reader) error { mg.Spec.ForProvider.Database = reference.ToPtrValue(rsp.ResolvedValue) mg.Spec.ForProvider.DatabaseRef = rsp.ResolvedReference + rsp, err = r.Resolve(ctx, reference.ResolutionRequest{ + CurrentValue: reference.FromPtrValue(mg.Spec.ForProvider.LoginDatabase), + Extract: reference.ExternalName(), + Reference: mg.Spec.ForProvider.LoginDatabaseRef, + Selector: mg.Spec.ForProvider.LoginDatabaseSelector, + To: reference.To{ + List: &DatabaseList{}, + Managed: &Database{}, + }, + }) + if err != nil { + return errors.Wrap(err, "mg.Spec.ForProvider.LoginDatabase") + } + mg.Spec.ForProvider.LoginDatabase = reference.ToPtrValue(rsp.ResolvedValue) + mg.Spec.ForProvider.LoginDatabaseRef = rsp.ResolvedReference + return nil } diff --git a/apis/mysql/v1alpha1/database_types.go b/apis/mysql/v1alpha1/database_types.go index 3b6fd865..ef6aac2c 100644 --- a/apis/mysql/v1alpha1/database_types.go +++ b/apis/mysql/v1alpha1/database_types.go @@ -37,7 +37,7 @@ type DatabaseStatus struct { type DatabaseParameters struct { // BinLog defines whether the create, delete, update operations of this database are propagated to replicas. Defaults to true // +optional - BinLog *bool `json:"binlog,omitempty" default:"true"` + BinLog *bool `json:"binlog,omitempty"` } // +kubebuilder:object:root=true diff --git a/apis/mysql/v1alpha1/grant_types.go b/apis/mysql/v1alpha1/grant_types.go index a3c5f12e..4c5dc4c5 100644 --- a/apis/mysql/v1alpha1/grant_types.go +++ b/apis/mysql/v1alpha1/grant_types.go @@ -95,7 +95,7 @@ type GrantParameters struct { // BinLog defines whether the create, delete, update operations of this grant are propagated to replicas. Defaults to true // +optional - BinLog *bool `json:"binlog,omitempty" default:"true"` + BinLog *bool `json:"binlog,omitempty"` } // A GrantStatus represents the observed state of a Grant. diff --git a/apis/mysql/v1alpha1/provider_types.go b/apis/mysql/v1alpha1/provider_types.go index b193f657..0b8c892d 100644 --- a/apis/mysql/v1alpha1/provider_types.go +++ b/apis/mysql/v1alpha1/provider_types.go @@ -31,9 +31,27 @@ type ProviderConfigSpec struct { // or use preferred to use TLS only when advertised by the server. This is similar // to skip-verify, but additionally allows a fallback to a connection which is // not encrypted. Neither skip-verify nor preferred add any reliable security. - // +kubebuilder:validation:Enum="true";skip-verify;preferred + // Alternatively, set tls=custom and provide a custom TLS configuration via the tlsConfig field. + // +kubebuilder:validation:Enum="true";skip-verify;preferred;custom // +optional TLS *string `json:"tls"` + + // Optional TLS configuration for sql driver. Setting this field also requires the tls field to be set to custom. + // +optional + TLSConfig *TLSConfig `json:"tlsConfig"` +} + +// TLSConfig defines the TLS configuration for the provider when tls=custom. +type TLSConfig struct { + CACert TLSSecret `json:"caCert,omitempty"` + ClientCert TLSSecret `json:"clientCert,omitempty"` + ClientKey TLSSecret `json:"clientKey,omitempty"` + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` +} + +// TLSSecret defines a reference to a K8s secret and its specific internal key that contains the TLS cert/keys in PEM format. +type TLSSecret struct { + SecretRef xpv1.SecretKeySelector `json:"secretRef,omitempty"` } const ( diff --git a/apis/mysql/v1alpha1/user_types.go b/apis/mysql/v1alpha1/user_types.go index 89fe9d98..069d34e9 100644 --- a/apis/mysql/v1alpha1/user_types.go +++ b/apis/mysql/v1alpha1/user_types.go @@ -48,7 +48,7 @@ type UserParameters struct { // BinLog defines whether the create, delete, update operations of this user are propagated to replicas. Defaults to true // +optional - BinLog *bool `json:"binlog,omitempty" default:"true"` + BinLog *bool `json:"binlog,omitempty"` } // ResourceOptions define the account specific resource limits. diff --git a/apis/mysql/v1alpha1/zz_generated.deepcopy.go b/apis/mysql/v1alpha1/zz_generated.deepcopy.go index 2f813766..4dfc6379 100644 --- a/apis/mysql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/mysql/v1alpha1/zz_generated.deepcopy.go @@ -397,6 +397,11 @@ func (in *ProviderConfigSpec) DeepCopyInto(out *ProviderConfigSpec) { *out = new(string) **out = **in } + if in.TLSConfig != nil { + in, out := &in.TLSConfig, &out.TLSConfig + *out = new(TLSConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigSpec. @@ -538,6 +543,40 @@ func (in *ResourceOptions) DeepCopy() *ResourceOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSConfig) DeepCopyInto(out *TLSConfig) { + *out = *in + out.CACert = in.CACert + out.ClientCert = in.ClientCert + out.ClientKey = in.ClientKey +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSConfig. +func (in *TLSConfig) DeepCopy() *TLSConfig { + if in == nil { + return nil + } + out := new(TLSConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSSecret) DeepCopyInto(out *TLSSecret) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSecret. +func (in *TLSSecret) DeepCopy() *TLSSecret { + if in == nil { + return nil + } + out := new(TLSSecret) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *User) DeepCopyInto(out *User) { *out = *in diff --git a/apis/postgresql/v1alpha1/grant_types.go b/apis/postgresql/v1alpha1/grant_types.go index 6d4c982d..1bf6fc7c 100644 --- a/apis/postgresql/v1alpha1/grant_types.go +++ b/apis/postgresql/v1alpha1/grant_types.go @@ -154,6 +154,10 @@ type GrantParameters struct { // +immutable // +optional MemberOfSelector *xpv1.Selector `json:"memberOfSelector,omitempty"` + + // RevokePublicOnDb apply the statement "REVOKE ALL ON DATABASE %s FROM PUBLIC" to make database unreachable from public + // +optional + RevokePublicOnDb *bool `json:"revokePublicOnDb,omitempty" default:"false"` } // A GrantStatus represents the observed state of a Grant. diff --git a/apis/postgresql/v1alpha1/schema_types.go b/apis/postgresql/v1alpha1/schema_types.go index a9a1a7a4..8f9a9d7f 100644 --- a/apis/postgresql/v1alpha1/schema_types.go +++ b/apis/postgresql/v1alpha1/schema_types.go @@ -59,6 +59,10 @@ type SchemaParameters struct { // +immutable // +optional DatabaseSelector *xpv1.Selector `json:"databaseSelector,omitempty"` + + // RevokePublicOnSchema apply a "REVOKE ALL ON SCHEMA public FROM public" statement + // +optional + RevokePublicOnSchema *bool `json:"revokePublicOnSchema,omitempty" default:"false"` } // A SchemaStatus represents the observed state of a Schema. diff --git a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go index 09ae90b7..70d15187 100644 --- a/apis/postgresql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/postgresql/v1alpha1/zz_generated.deepcopy.go @@ -426,6 +426,11 @@ func (in *GrantParameters) DeepCopyInto(out *GrantParameters) { *out = new(v1.Selector) (*in).DeepCopyInto(*out) } + if in.RevokePublicOnDb != nil { + in, out := &in.RevokePublicOnDb, &out.RevokePublicOnDb + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrantParameters. @@ -978,6 +983,11 @@ func (in *SchemaParameters) DeepCopyInto(out *SchemaParameters) { *out = new(v1.Selector) (*in).DeepCopyInto(*out) } + if in.RevokePublicOnSchema != nil { + in, out := &in.RevokePublicOnSchema, &out.RevokePublicOnSchema + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SchemaParameters. diff --git a/build b/build index 3b994632..92457ef1 160000 --- a/build +++ b/build @@ -1 +1 @@ -Subproject commit 3b99463225581259ce39c7d7a45290be12515abb +Subproject commit 92457ef1c0feb75cd235bdb90244e340d0796b7f diff --git a/cluster/local/integration_tests.sh b/cluster/local/integration_tests.sh index 06661871..e8669b78 100755 --- a/cluster/local/integration_tests.sh +++ b/cluster/local/integration_tests.sh @@ -7,27 +7,27 @@ YLW='\033[0;33m' GRN='\033[0;32m' RED='\033[0;31m' NOC='\033[0m' # No Color -echo_info(){ +echo_info() { printf "\n${BLU}%s${NOC}" "$1" } -echo_step(){ +echo_step() { printf "\n${BLU}>>>>>>> %s${NOC}\n" "$1" } -echo_sub_step(){ +echo_sub_step() { printf "\n${BLU}>>> %s${NOC}\n" "$1" } -echo_step_completed(){ +echo_step_completed() { printf "${GRN} [✔]${NOC}" } -echo_success(){ +echo_success() { printf "\n${GRN}%s${NOC}\n" "$1" } -echo_warn(){ +echo_warn() { printf "\n${YLW}%s${NOC}" "$1" } -echo_error(){ +echo_error() { printf "\n${RED}%s${NOC}" "$1" exit 1 } @@ -47,51 +47,71 @@ version_tag="$(cat ${projectdir}/_output/version)" # tag as latest version to load into kind cluster K8S_CLUSTER="${K8S_CLUSTER:-${BUILD_REGISTRY}-inttests}" -CROSSPLANE_NAMESPACE="crossplane-system" PACKAGE_NAME="provider-sql" +MARIADB_ROOT_PW=$(openssl rand -base64 32) +MARIADB_TEST_PW=$(openssl rand -base64 32) # cleanup on exit if [ "$skipcleanup" != true ]; then function cleanup { echo_step "Cleaning up..." export KUBECONFIG= - "${KIND}" delete cluster --name="${K8S_CLUSTER}" + cleanup_cluster } trap cleanup EXIT fi -# setup package cache -echo_step "setting up local package cache" -CACHE_PATH="${projectdir}/.work/inttest-package-cache" -mkdir -p "${CACHE_PATH}" -echo "created cache dir at ${CACHE_PATH}" -"${UP}" alpha xpkg xp-extract --from-xpkg "${OUTPUT_DIR}"/xpkg/linux_"${SAFEHOSTARCH}"/"${PACKAGE_NAME}"-"${VERSION}".xpkg -o "${CACHE_PATH}/${PACKAGE_NAME}.gz" && chmod 644 "${CACHE_PATH}/${PACKAGE_NAME}.gz" - -# create kind cluster with extra mounts -KIND_NODE_IMAGE="kindest/node:${KIND_NODE_IMAGE_TAG}" -echo_step "creating k8s cluster using kind ${KIND_VERSION} and node image ${KIND_NODE_IMAGE}" -KIND_CONFIG="$( cat </dev/null + "${HELM}" repo update + "${HELM}" install mariadb bitnami/mariadb \ + --version 11.3.0 \ + --set auth.rootPassword="${MARIADB_ROOT_PW}" \ + --wait +} + +setup_mariadb_tls() { + echo_step "installing MariaDB with TLS" + "${KUBECTL}" create secret generic mariadb-creds \ + --from-literal username="test" \ + --from-literal password="${MARIADB_TEST_PW}" \ + --from-literal endpoint="mariadb.default.svc.cluster.local" \ + --from-literal port="3306" \ + --from-file=ca-cert.pem \ + --from-file=client-cert.pem \ + --from-file=client-key.pem + + local values=$(cat < $current ]]; then - echo_error "timeout of ${timeout}s has been reached" - fi - sleep $step; -done - -echo_success "Integration tests succeeded!" + ) + + "${HELM}" repo add bitnami https://charts.bitnami.com/bitnami >/dev/null + "${HELM}" repo update + "${HELM}" install mariadb bitnami/mariadb \ + --version 11.3.0 \ + --values <(echo "$values") \ + --wait +} + +cleanup_mariadb() { + echo_step "uninstalling MariaDB" + "${HELM}" uninstall mariadb + "${KUBECTL}" delete secret mariadb-creds +} + +test_create_database() { + echo_step "test creating MySQL Database resource" + "${KUBECTL}" apply -f ${projectdir}/examples/mysql/database.yaml + + echo_info "check if is ready" + "${KUBECTL}" wait --timeout 2m --for condition=Ready -f ${projectdir}/examples/mysql/database.yaml + echo_step_completed +} + +test_create_user() { + echo_step "test creating MySQL User resource" + local user_pw="asdf1234" + "${KUBECTL}" create secret generic example-pw --from-literal password="${user_pw}" + "${KUBECTL}" apply -f ${projectdir}/examples/mysql/user.yaml + + echo_info "check if is ready" + "${KUBECTL}" wait --timeout 2m --for condition=Ready -f ${projectdir}/examples/mysql/user.yaml + echo_step_completed + + echo_info "check if connection secret exists" + local pw=$("${KUBECTL}" get secret example-connection-secret -ojsonpath='{.data.password}' | base64 --decode) + [ "${pw}" == "${user_pw}" ] + echo_step_completed +} + +test_update_user_password() { + echo_step "test updating MySQL User password" + local user_pw="newpassword" + "${KUBECTL}" create secret generic example-pw --from-literal password="${user_pw}" --dry-run -oyaml | \ + "${KUBECTL}" apply -f - + + # trigger reconcile + "${KUBECTL}" annotate -f ${projectdir}/examples/mysql/user.yaml reconcile=now + + sleep 3 + + echo_info "check if connection secret has been updated" + local pw=$("${KUBECTL}" get secret example-connection-secret -ojsonpath='{.data.password}' | base64 --decode) + [ "${pw}" == "${user_pw}" ] + echo_step_completed +} + +test_create_grant() { + echo_step "test creating MySQL Grant resource" + "${KUBECTL}" apply -f ${projectdir}/examples/mysql/grant_database.yaml + + echo_info "check if is ready" + "${KUBECTL}" wait --timeout 2m --for condition=Ready -f ${projectdir}/examples/mysql/grant_database.yaml + echo_step_completed +} + +test_all() { + test_create_database + test_create_user + test_update_user_password + test_create_grant +} + +cleanup_test_resources() { + echo_step "cleaning up test resources" + "${KUBECTL}" delete -f ${projectdir}/examples/mysql/grant_database.yaml + "${KUBECTL}" delete -f ${projectdir}/examples/mysql/database.yaml + "${KUBECTL}" delete -f ${projectdir}/examples/mysql/user.yaml + "${KUBECTL}" delete secret example-pw +} + +setup_cluster +setup_crossplane +setup_provider + +echo_step "--- INTEGRATION TESTS - NO TLS ---" + +setup_mariadb_no_tls +setup_provider_config_no_tls + +test_all + +cleanup_test_resources +cleanup_provider_config +cleanup_mariadb + +echo_step "--- INTEGRATION TESTS - TLS ---" + +setup_tls_certs +setup_mariadb_tls +setup_provider_config_tls + +test_all + +cleanup_test_resources +cleanup_provider_config +cleanup_mariadb +cleanup_tls_certs + +echo_step "--- INTEGRATION TESTS FOR MySQL ACCOMPLISHED SUCCESSFULLY ---" + +echo_step "--- TESTING POSTGRESDB ---" +integration_tests_postgres +echo_step "--- INTEGRATION TESTS FOR POSTGRESDB ACCOMPLISHED SUCCESSFULLY ---" + +integration_tests_end \ No newline at end of file diff --git a/cluster/local/postgresdb_functions.sh b/cluster/local/postgresdb_functions.sh new file mode 100644 index 00000000..4a64c7e2 --- /dev/null +++ b/cluster/local/postgresdb_functions.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +set -e + +setup_postgresdb_no_tls() { + echo_step "Installing PostgresDB Helm chart into default namespace" + postgres_root_pw=$(LC_ALL=C tr -cd "A-Za-z0-9" 0 { sort.Strings(toRevoke) - query := fmt.Sprintf("REVOKE %s FROM %s", - strings.Join(toRevoke, ", "), mssql.QuoteIdentifier(*cr.Spec.ForProvider.User)) + query := fmt.Sprintf("REVOKE %s %s FROM %s", + strings.Join(toRevoke, ", "), onSchemaQuery(cr), mssql.QuoteIdentifier(*cr.Spec.ForProvider.User)) if err = c.db.Exec(ctx, xsql.Query{String: query}); err != nil { return managed.ExternalUpdate{}, errors.Wrap(err, errRevoke) } } if len(toGrant) > 0 { sort.Strings(toGrant) - query := fmt.Sprintf("GRANT %s TO %s", - strings.Join(toGrant, ", "), mssql.QuoteIdentifier(*cr.Spec.ForProvider.User)) + query := fmt.Sprintf("GRANT %s %s TO %s", + strings.Join(toGrant, ", "), onSchemaQuery(cr), mssql.QuoteIdentifier(*cr.Spec.ForProvider.User)) if err = c.db.Exec(ctx, xsql.Query{String: query}); err != nil { return managed.ExternalUpdate{}, errors.Wrap(err, errGrant) } @@ -200,23 +207,47 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { username := *cr.Spec.ForProvider.User - query := fmt.Sprintf("REVOKE %s FROM %s", + query := fmt.Sprintf("REVOKE %s %s FROM %s", strings.Join(cr.Spec.ForProvider.Permissions.ToStringSlice(), ", "), + onSchemaQuery(cr), mssql.QuoteIdentifier(username), ) return errors.Wrap(c.db.Exec(ctx, xsql.Query{String: query}), errRevoke) } -func (c *external) getPermissions(ctx context.Context, username string) ([]string, error) { - // TODO(turkenh/ulucinar): Possible performance improvement. We first - // calculate the Cartesian product, and then filter. It would be more - // efficient to first filter principals by name, and then join. - query := fmt.Sprintf(`SELECT pe.permission_name - FROM sys.database_principals AS pr - JOIN sys.database_permissions AS pe - ON pe.grantee_principal_id = pr.principal_id +// TODO(turkenh/ulucinar): Possible performance improvement. We first +// +// calculate the Cartesian product, and then filter. It would be more +// efficient to first filter principals by name, and then join. +const queryPermissionDefault = `SELECT pe.permission_name + FROM sys.database_principals AS pr + JOIN sys.database_permissions AS pe + ON pe.grantee_principal_id = pr.principal_id + WHERE + pe.class = 0 /* DATABASE (default) */ + AND pr.name = %s` + +const queryPermissionSchema = `SELECT pe.permission_name + FROM sys.database_principals AS pr + JOIN sys.database_permissions AS pe + ON pe.grantee_principal_id = pr.principal_id + JOIN sys.schemas AS s + ON s.schema_id = pe.major_id WHERE - pr.name = %s`, mssql.QuoteValue(username)) + pe.class = 3 /* SCHEMA */ + AND s.name = %s + AND pr.name = %s` + +func (c *external) getPermissions(ctx context.Context, cr *v1alpha1.Grant) ([]string, error) { + var query string + if cr.Spec.ForProvider.Schema == nil { + query = fmt.Sprintf(queryPermissionDefault, mssql.QuoteValue(*cr.Spec.ForProvider.User)) + } else { + query = fmt.Sprintf(queryPermissionSchema, + mssql.QuoteValue(*cr.Spec.ForProvider.Schema), + mssql.QuoteValue(*cr.Spec.ForProvider.User), + ) + } rows, err := c.db.Query(ctx, xsql.Query{String: query}) if err != nil { return nil, errors.Wrap(err, errCannotGetGrants) @@ -237,6 +268,13 @@ func (c *external) getPermissions(ctx context.Context, username string) ([]strin return permissions, nil } +func onSchemaQuery(cr *v1alpha1.Grant) (schema string) { + if cr.Spec.ForProvider.Schema != nil { + schema = fmt.Sprintf("ON SCHEMA::%s", *cr.Spec.ForProvider.Schema) + } + return +} + func diffPermissions(desired, observed []string) ([]string, []string) { md := make(map[string]struct{}, len(desired)) mo := make(map[string]struct{}, len(observed)) diff --git a/pkg/controller/mssql/grant/reconciler_test.go b/pkg/controller/mssql/grant/reconciler_test.go index 7227f100..c4b9d079 100644 --- a/pkg/controller/mssql/grant/reconciler_test.go +++ b/pkg/controller/mssql/grant/reconciler_test.go @@ -266,6 +266,9 @@ func TestObserve(t *testing.T) { fields: fields{ db: mockDB{ MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) { + if strings.Contains(q.String, "sys.schemas") { + return nil, errBoom + } return mockRowsToSQLRows( sqlmock.NewRows( []string{"Grants"}, @@ -293,6 +296,42 @@ func TestObserve(t *testing.T) { err: nil, }, }, + "SuccessSchema": { + reason: "We should return no error if we can successfully get our permissions", + fields: fields{ + db: mockDB{ + MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) { + if !strings.Contains(q.String, "sys.schemas") { + return nil, errBoom + } + return mockRowsToSQLRows( + sqlmock.NewRows( + []string{"Grants"}, + ).AddRow("ALTER"), + ), nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("success-db"), + User: ptr.To("success-user"), + Schema: ptr.To("success-schema"), + Permissions: v1alpha1.GrantPermissions{"ALTER"}, + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + err: nil, + }, + }, "SuccessDiffPermissions": { reason: "We should return no error if different permissions exist", fields: fields{ @@ -430,7 +469,12 @@ func TestCreate(t *testing.T) { reason: "No error should be returned when we successfully create a grant", fields: fields{ db: &mockDB{ - MockExec: func(ctx context.Context, q xsql.Query) error { return nil }, + MockExec: func(ctx context.Context, q xsql.Query) error { + if strings.Contains(q.String, "ON SCHEMA::") { + return errBoom + } + return nil + }, }, }, args: args{ @@ -448,6 +492,34 @@ func TestCreate(t *testing.T) { err: nil, }, }, + "SuccessSchema": { + reason: "No error should be returned when we successfully create a grant", + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { + if !strings.Contains(q.String, "ON SCHEMA::") { + return errBoom + } + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("test-example"), + User: ptr.To("test-example"), + Schema: ptr.To("success-schema"), + Permissions: v1alpha1.GrantPermissions{"ALTER"}, + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, } for name, tc := range cases { @@ -529,6 +601,9 @@ func TestUpdate(t *testing.T) { return mockRowsToSQLRows(sqlmock.NewRows([]string{})), nil }, MockExec: func(ctx context.Context, q xsql.Query) error { + if strings.Contains(q.String, "ON SCHEMA::") { + return errBoom + } if strings.Contains(q.String, "CREATE, DELETE") { return nil } @@ -552,6 +627,41 @@ func TestUpdate(t *testing.T) { c: managed.ExternalUpdate{}, }, }, + "SuccessSchema": { + reason: "No error should be returned when we update a grant", + fields: fields{ + db: &mockDB{ + MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) { + return mockRowsToSQLRows(sqlmock.NewRows([]string{})), nil + }, + MockExec: func(ctx context.Context, q xsql.Query) error { + if !strings.Contains(q.String, "ON SCHEMA::") { + return errBoom + } + if strings.Contains(q.String, "ALTER") { + return nil + } + return errBoom + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("test-example"), + User: ptr.To("test-example"), + Schema: ptr.To("success-schema"), + Permissions: v1alpha1.GrantPermissions{"ALTER"}, + }, + }, + }, + }, + want: want{ + err: nil, + c: managed.ExternalUpdate{}, + }, + }, } for name, tc := range cases { @@ -630,7 +740,37 @@ func TestDelete(t *testing.T) { }, fields: fields{ db: &mockDB{ - MockExec: func(ctx context.Context, q xsql.Query) error { return nil }, + MockExec: func(ctx context.Context, q xsql.Query) error { + if strings.Contains(q.String, "ON SCHEMA::") { + return errBoom + } + return nil + }, + }, + }, + want: nil, + }, + "SuccessSchema": { + reason: "No error should be returned if the grant was revoked", + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Database: ptr.To("test-example"), + User: ptr.To("test-example"), + Schema: ptr.To("success-schema"), + }, + }, + }, + }, + fields: fields{ + db: &mockDB{ + MockExec: func(ctx context.Context, q xsql.Query) error { + if !strings.Contains(q.String, "ON SCHEMA::") { + return errBoom + } + return nil + }, }, }, want: nil, @@ -739,11 +879,11 @@ func Test_diffPermissions(t *testing.T) { } for name, tc := range cases { t.Run(name, func(t *testing.T) { - gotToGrant, gotToRevoke := diffPermissions(tc.args.desired, tc.args.observed) - if diff := cmp.Diff(tc.want.toGrant, gotToGrant, equateSlices()); diff != "" { + gotToGrant, gotToRevoke := diffPermissions(tc.desired, tc.observed) + if diff := cmp.Diff(tc.toGrant, gotToGrant, equateSlices()); diff != "" { t.Errorf("\ndiffPermissions(...): -want toGrant, +got toGrant:\n%s", diff) } - if diff := cmp.Diff(tc.want.toRevoke, gotToRevoke, equateSlices()); diff != "" { + if diff := cmp.Diff(tc.toRevoke, gotToRevoke, equateSlices()); diff != "" { t.Errorf("\ndiffPermissions(...): -want toRevoke, +got toRevoke:\n%s", diff) } }) diff --git a/pkg/controller/mssql/user/reconciler.go b/pkg/controller/mssql/user/reconciler.go index 5ab1d202..c8cf357f 100644 --- a/pkg/controller/mssql/user/reconciler.go +++ b/pkg/controller/mssql/user/reconciler.go @@ -31,6 +31,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/feature" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/password" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" @@ -67,13 +68,19 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { name := managed.ControllerName(v1alpha1.UserGroupKind) t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.UserGroupVersionKind), + reconcilerOptions := []managed.ReconcilerOption{ managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newClient: mssql.New}), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) - + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.UserGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.User{}). @@ -119,15 +126,23 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E return nil, errors.Wrap(err, errGetSecret) } + userDB := c.newClient(s.Data, ptr.Deref(cr.Spec.ForProvider.Database, "")) + loginDB := userDB + if cr.Spec.ForProvider.LoginDatabase != nil { + loginDB = c.newClient(s.Data, ptr.Deref(cr.Spec.ForProvider.LoginDatabase, "")) + } + return &external{ - db: c.newClient(s.Data, ptr.Deref(cr.Spec.ForProvider.Database, "")), - kube: c.kube, + userDB: userDB, + loginDB: loginDB, + kube: c.kube, }, nil } type external struct { - db xsql.DB - kube client.Client + userDB xsql.DB + loginDB xsql.DB + kube client.Client } func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { @@ -139,7 +154,7 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex var name string query := "SELECT name FROM sys.database_principals WHERE type = 'S' AND name = @p1" - err := c.db.Scan(ctx, xsql.Query{ + err := c.userDB.Scan(ctx, xsql.Query{ String: query, Parameters: []interface{}{ meta.GetExternalName(cr), }, @@ -182,21 +197,21 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } loginQuery := fmt.Sprintf("CREATE LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(cr)), mssql.QuoteValue(pw)) - if err := c.db.Exec(ctx, xsql.Query{ + if err := c.loginDB.Exec(ctx, xsql.Query{ String: loginQuery, }); err != nil { return managed.ExternalCreation{}, errors.Wrapf(err, errCreateLogin, meta.GetExternalName(cr)) } userQuery := fmt.Sprintf("CREATE USER %s FOR LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(cr)), mssql.QuoteIdentifier(meta.GetExternalName(cr))) - if err := c.db.Exec(ctx, xsql.Query{ + if err := c.userDB.Exec(ctx, xsql.Query{ String: userQuery, }); err != nil { return managed.ExternalCreation{}, errors.Wrapf(err, errCreateUser, meta.GetExternalName(cr)) } return managed.ExternalCreation{ - ConnectionDetails: c.db.GetConnectionDetails(meta.GetExternalName(cr), pw), + ConnectionDetails: c.userDB.GetConnectionDetails(meta.GetExternalName(cr), pw), }, nil } @@ -213,14 +228,14 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext if changed { query := fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD=%s", mssql.QuoteIdentifier(meta.GetExternalName(cr)), mssql.QuoteValue(pw)) - if err := c.db.Exec(ctx, xsql.Query{ + if err := c.loginDB.Exec(ctx, xsql.Query{ String: query, }); err != nil { return managed.ExternalUpdate{}, errors.Wrap(err, errUpdateUser) } return managed.ExternalUpdate{ - ConnectionDetails: c.db.GetConnectionDetails(meta.GetExternalName(cr), pw), + ConnectionDetails: c.userDB.GetConnectionDetails(meta.GetExternalName(cr), pw), }, nil } return managed.ExternalUpdate{}, nil @@ -233,7 +248,7 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { } query := fmt.Sprintf("SELECT session_id FROM sys.dm_exec_sessions WHERE login_name = %s", mssql.QuoteValue(meta.GetExternalName(cr))) - rows, err := c.db.Query(ctx, xsql.Query{String: query}) + rows, err := c.userDB.Query(ctx, xsql.Query{String: query}) if err != nil { return errors.Wrap(err, errCannotGetLogins) } @@ -244,7 +259,7 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { if err := rows.Scan(&sessionID); err != nil { return errors.Wrap(err, errCannotGetLogins) } - if err := c.db.Exec(ctx, xsql.Query{String: fmt.Sprintf("KILL %d", sessionID)}); err != nil { + if err := c.userDB.Exec(ctx, xsql.Query{String: fmt.Sprintf("KILL %d", sessionID)}); err != nil { return errors.Wrapf(err, errCannotKillLoginSession, sessionID, meta.GetExternalName(cr)) } } @@ -252,13 +267,13 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { return errors.Wrap(err, errCannotGetLogins) } - if err := c.db.Exec(ctx, xsql.Query{ + if err := c.userDB.Exec(ctx, xsql.Query{ String: fmt.Sprintf("DROP USER IF EXISTS %s", mssql.QuoteIdentifier(meta.GetExternalName(cr))), }); err != nil { return errors.Wrapf(err, errDropUser, meta.GetExternalName(cr)) } - if err := c.db.Exec(ctx, xsql.Query{ + if err := c.loginDB.Exec(ctx, xsql.Query{ String: fmt.Sprintf("DROP LOGIN %s", mssql.QuoteIdentifier(meta.GetExternalName(cr))), }); err != nil { return errors.Wrapf(err, errDropLogin, meta.GetExternalName(cr)) diff --git a/pkg/controller/mssql/user/reconciler_test.go b/pkg/controller/mssql/user/reconciler_test.go index a0a3a74c..022eceb9 100644 --- a/pkg/controller/mssql/user/reconciler_test.go +++ b/pkg/controller/mssql/user/reconciler_test.go @@ -28,6 +28,7 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" @@ -40,6 +41,7 @@ import ( ) type mockDB struct { + database string MockExec func(ctx context.Context, q xsql.Query) error MockExecTx func(ctx context.Context, ql []xsql.Query) error MockScan func(ctx context.Context, q xsql.Query, dest ...interface{}) error @@ -92,18 +94,25 @@ func TestConnect(t *testing.T) { mg resource.Managed } + type want struct { + sameClient *bool + err error + } + cases := map[string]struct { reason string fields fields args args - want error + want want }{ "ErrNotUser": { reason: "An error should be returned if the managed resource is not a *User", args: args{ mg: nil, }, - want: errors.New(errNotUser), + want: want{ + err: errors.New(errNotUser), + }, }, "ErrTrackProviderConfigUsage": { reason: "An error should be returned if we can't track our ProviderConfig usage", @@ -113,7 +122,9 @@ func TestConnect(t *testing.T) { args: args{ mg: &v1alpha1.User{}, }, - want: errors.Wrap(errBoom, errTrackPCUsage), + want: want{ + err: errors.Wrap(errBoom, errTrackPCUsage), + }, }, "ErrGetProviderConfig": { reason: "An error should be returned if we can't get our ProviderConfig", @@ -132,7 +143,9 @@ func TestConnect(t *testing.T) { }, }, }, - want: errors.Wrap(errBoom, errGetPC), + want: want{ + err: errors.Wrap(errBoom, errGetPC), + }, }, "ErrMissingConnectionSecret": { reason: "An error should be returned if our ProviderConfig doesn't specify a connection secret", @@ -154,7 +167,9 @@ func TestConnect(t *testing.T) { }, }, }, - want: errors.New(errNoSecretRef), + want: want{ + err: errors.New(errNoSecretRef), + }, }, "ErrGetConnectionSecret": { reason: "An error should be returned if we can't get our ProviderConfig's connection secret", @@ -181,17 +196,106 @@ func TestConnect(t *testing.T) { }, }, }, - want: errors.Wrap(errBoom, errGetSecret), + want: want{ + err: errors.Wrap(errBoom, errGetSecret), + }, + }, + "Success": { + reason: "With NO login database defined, the clients should be the same", + fields: fields{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + switch o := obj.(type) { + case *v1alpha1.ProviderConfig: + o.Spec.Credentials.ConnectionSecretRef = &xpv1.SecretReference{} + case *corev1.Secret: + secret := corev1.Secret{ + Data: map[string][]byte{}, + } + secret.DeepCopyInto(obj.(*corev1.Secret)) + } + return nil + }), + }, + usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), + newDB: func(creds map[string][]byte, database string) xsql.DB { return mockDB{database: database} }, + }, + args: args{ + mg: &v1alpha1.User{ + Spec: v1alpha1.UserSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{}, + }, + ForProvider: v1alpha1.UserParameters{ + Database: ptr.To("success-database"), + }, + }, + }, + }, + want: want{ + err: nil, + sameClient: ptr.To(true), + }, + }, + "SuccessLoginDB": { + reason: "With the login database defined, the clients should differ", + fields: fields{ + kube: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + switch o := obj.(type) { + case *v1alpha1.ProviderConfig: + o.Spec.Credentials.ConnectionSecretRef = &xpv1.SecretReference{} + case *corev1.Secret: + secret := corev1.Secret{ + Data: map[string][]byte{}, + } + secret.DeepCopyInto(obj.(*corev1.Secret)) + } + return nil + }), + }, + usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), + newDB: func(creds map[string][]byte, database string) xsql.DB { return mockDB{database: database} }, + }, + args: args{ + mg: &v1alpha1.User{ + Spec: v1alpha1.UserSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{}, + }, + ForProvider: v1alpha1.UserParameters{ + Database: ptr.To("success-database"), + LoginDatabase: ptr.To("success-login-database"), + }, + }, + }, + }, + want: want{ + err: nil, + sameClient: ptr.To(false), + }, }, } for name, tc := range cases { t.Run(name, func(t *testing.T) { e := &connector{kube: tc.fields.kube, usage: tc.fields.usage, newClient: tc.fields.newDB} - _, err := e.Connect(tc.args.ctx, tc.args.mg) - if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { + ec, err := e.Connect(tc.args.ctx, tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\ne.Connect(...): -want error, +got error:\n%s\n", tc.reason, diff) } + if tc.want.sameClient != nil { + ext := ec.(*external) + db1 := ext.userDB.(mockDB).database + db2 := ext.loginDB.(mockDB).database + if *tc.want.sameClient && db1 != db2 { + t.Errorf("\n%s\ne.Connect(...): want clients to be on the same database\n%s / %s\n", + tc.reason, db1, db2) + } else if !*tc.want.sameClient && db1 == db2 { + t.Errorf("\n%s\ne.Connect(...): want clients NOT to be the same instance\n%s\n", + tc.reason, db1) + } + } }) } } @@ -325,8 +429,9 @@ func TestObserve(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { e := external{ - db: tc.fields.db, - kube: tc.fields.kube, + userDB: tc.fields.db, + loginDB: tc.fields.db, + kube: tc.fields.kube, } got, err := e.Observe(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { @@ -474,8 +579,9 @@ func TestCreate(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { e := external{ - db: tc.fields.db, - kube: tc.fields.kube, + userDB: tc.fields.db, + loginDB: tc.fields.db, + kube: tc.fields.kube, } got, err := e.Create(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { @@ -692,8 +798,9 @@ func TestUpdate(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { e := external{ - db: tc.fields.db, - kube: tc.args.kube, + userDB: tc.fields.db, + loginDB: tc.fields.db, + kube: tc.args.kube, } got, err := e.Update(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { @@ -710,7 +817,8 @@ func TestDelete(t *testing.T) { errBoom := errors.New("boom") type fields struct { - db xsql.DB + userDB xsql.DB + loginDB xsql.DB } type args struct { @@ -734,7 +842,7 @@ func TestDelete(t *testing.T) { "ErrDropDB": { reason: "Errors dropping a user should be returned", fields: fields{ - db: &mockDB{ + userDB: &mockDB{ MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) { return mockRowsToSQLRows(sqlmock.NewRows([]string{})), nil }, @@ -742,6 +850,7 @@ func TestDelete(t *testing.T) { return errBoom }, }, + loginDB: &mockDB{}, }, args: args{ mg: &v1alpha1.User{}, @@ -751,7 +860,7 @@ func TestDelete(t *testing.T) { "Success": { reason: "No error should be returned", fields: fields{ - db: &mockDB{ + userDB: &mockDB{ MockQuery: func(ctx context.Context, q xsql.Query) (*sql.Rows, error) { return mockRowsToSQLRows(sqlmock.NewRows([]string{})), nil }, @@ -759,6 +868,12 @@ func TestDelete(t *testing.T) { return nil }, }, + loginDB: &mockDB{ + + MockExec: func(ctx context.Context, q xsql.Query) error { + return nil + }, + }, }, args: args{ mg: &v1alpha1.User{}, @@ -768,7 +883,7 @@ func TestDelete(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - e := external{db: tc.fields.db} + e := external{userDB: tc.fields.userDB, loginDB: tc.fields.loginDB} err := e.Delete(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\ne.Delete(...): -want error, +got error:\n%s\n", tc.reason, diff) diff --git a/pkg/controller/mysql/database/reconciler.go b/pkg/controller/mysql/database/reconciler.go index 2aab4db6..d5d8c932 100644 --- a/pkg/controller/mysql/database/reconciler.go +++ b/pkg/controller/mysql/database/reconciler.go @@ -22,7 +22,6 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -30,6 +29,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/feature" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" @@ -37,6 +37,7 @@ import ( "github.com/crossplane-contrib/provider-sql/apis/mysql/v1alpha1" "github.com/crossplane-contrib/provider-sql/pkg/clients/mysql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" + "github.com/crossplane-contrib/provider-sql/pkg/controller/mysql/tls" ) const ( @@ -44,6 +45,7 @@ const ( errGetPC = "cannot get ProviderConfig" errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" + errTLSConfig = "cannot load TLS config" errNotDatabase = "managed resource is not a Database custom resource" errSelectDB = "cannot select database" @@ -58,12 +60,19 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { name := managed.ControllerName(v1alpha1.DatabaseGroupKind) t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.DatabaseGroupVersionKind), + reconcilerOptions := []managed.ReconcilerOption{ managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: mysql.New}), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.DatabaseGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). @@ -77,7 +86,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { type connector struct { kube client.Client usage resource.Tracker - newDB func(creds map[string][]byte, tls *string) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB } func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { @@ -92,8 +101,9 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E // ProviderConfigReference could theoretically be nil, but in practice the // DefaultProviderConfig initializer will set it before we get here. + providerConfigName := cr.GetProviderConfigReference().Name pc := &v1alpha1.ProviderConfig{} - if err := c.kube.Get(ctx, types.NamespacedName{Name: cr.GetProviderConfigReference().Name}, pc); err != nil { + if err := c.kube.Get(ctx, types.NamespacedName{Name: providerConfigName}, pc); err != nil { return nil, errors.Wrap(err, errGetPC) } @@ -110,7 +120,12 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E return nil, errors.Wrap(err, errGetSecret) } - return &external{db: c.newDB(s.Data, pc.Spec.TLS)}, nil + tlsName, err := tls.LoadConfig(ctx, c.kube, providerConfigName, pc.Spec.TLS, pc.Spec.TLSConfig) + if err != nil { + return nil, errors.Wrap(err, errTLSConfig) + } + + return &external{db: c.newDB(s.Data, tlsName, cr.Spec.ForProvider.BinLog)}, nil } type external struct{ db xsql.DB } @@ -148,10 +163,9 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalCreation{}, errors.New(errNotDatabase) } - binlog := cr.Spec.ForProvider.BinLog query := "CREATE DATABASE " + mysql.QuoteIdentifier(meta.GetExternalName(cr)) - if err := mysql.ExecWithBinlogAndFlush(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errCreateDB}, mysql.ExecOptions{Binlog: binlog, Flush: ptr.To(false)}); err != nil { + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errCreateDB}); err != nil { return managed.ExternalCreation{}, err } @@ -169,10 +183,9 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { return errors.New(errNotDatabase) } - binlog := cr.Spec.ForProvider.BinLog query := "DROP DATABASE IF EXISTS " + mysql.QuoteIdentifier(meta.GetExternalName(cr)) - if err := mysql.ExecWithBinlogAndFlush(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errDropDB}, mysql.ExecOptions{Binlog: binlog, Flush: ptr.To(false)}); err != nil { + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errDropDB}); err != nil { return err } diff --git a/pkg/controller/mysql/database/reconciler_test.go b/pkg/controller/mysql/database/reconciler_test.go index 6ae33c78..e1a6a59a 100644 --- a/pkg/controller/mysql/database/reconciler_test.go +++ b/pkg/controller/mysql/database/reconciler_test.go @@ -64,7 +64,7 @@ func TestConnect(t *testing.T) { type fields struct { kube client.Client usage resource.Tracker - newDB func(creds map[string][]byte, tls *string) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB } type args struct { diff --git a/pkg/controller/mysql/grant/reconciler.go b/pkg/controller/mysql/grant/reconciler.go index a4d071b8..675ab401 100644 --- a/pkg/controller/mysql/grant/reconciler.go +++ b/pkg/controller/mysql/grant/reconciler.go @@ -34,12 +34,14 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/feature" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/crossplane-contrib/provider-sql/apis/mysql/v1alpha1" "github.com/crossplane-contrib/provider-sql/pkg/clients/mysql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" + "github.com/crossplane-contrib/provider-sql/pkg/controller/mysql/tls" ) const ( @@ -47,6 +49,7 @@ const ( errGetPC = "cannot get ProviderConfig" errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" + errTLSConfig = "cannot load TLS config" errNotGrant = "managed resource is not a Grant custom resource" errCreateGrant = "cannot create grant" @@ -67,14 +70,20 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { name := managed.ControllerName(v1alpha1.GrantGroupKind) t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.GrantGroupVersionKind), + reconcilerOptions := []managed.ReconcilerOption{ managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: mysql.New}), managed.WithReferenceResolver(managed.NewAPISimpleReferenceResolver(mgr.GetClient())), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) - + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.GrantGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.Grant{}). @@ -87,7 +96,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { type connector struct { kube client.Client usage resource.Tracker - newDB func(creds map[string][]byte, tls *string) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB } func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { @@ -102,8 +111,9 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E // ProviderConfigReference could theoretically be nil, but in practice the // DefaultProviderConfig initializer will set it before we get here. + providerConfigName := cr.GetProviderConfigReference().Name pc := &v1alpha1.ProviderConfig{} - if err := c.kube.Get(ctx, types.NamespacedName{Name: cr.GetProviderConfigReference().Name}, pc); err != nil { + if err := c.kube.Get(ctx, types.NamespacedName{Name: providerConfigName}, pc); err != nil { return nil, errors.Wrap(err, errGetPC) } @@ -120,8 +130,13 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E return nil, errors.Wrap(err, errGetSecret) } + tlsName, err := tls.LoadConfig(ctx, c.kube, providerConfigName, pc.Spec.TLS, pc.Spec.TLSConfig) + if err != nil { + return nil, errors.Wrap(err, errTLSConfig) + } + return &external{ - db: c.newDB(s.Data, pc.Spec.TLS), + db: c.newDB(s.Data, tlsName, cr.Spec.ForProvider.BinLog), kube: c.kube, }, nil } @@ -137,11 +152,11 @@ func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.Ex return managed.ExternalObservation{}, errors.New(errNotGrant) } - username := *cr.Spec.ForProvider.User + username, host := mysql.SplitUserHost(*cr.Spec.ForProvider.User) dbname := defaultIdentifier(cr.Spec.ForProvider.Database) table := defaultIdentifier(cr.Spec.ForProvider.Table) - observedPrivileges, result, err := c.getPrivileges(ctx, username, dbname, table) + observedPrivileges, result, err := c.getPrivileges(ctx, username, host, dbname, table) if err != nil { return managed.ExternalObservation{}, err } @@ -185,9 +200,7 @@ func parseGrant(grant, dbname string, table string) (privileges []string) { return nil } -func (c *external) getPrivileges(ctx context.Context, username, dbname string, table string) ([]string, *managed.ExternalObservation, error) { - username, host := mysql.SplitUserHost(username) - +func (c *external) getPrivileges(ctx context.Context, username, host, dbname, table string) ([]string, *managed.ExternalObservation, error) { privileges, err := c.parseGrantRows(ctx, username, host, dbname, table) if err != nil { var myErr *mysqldriver.MySQLError @@ -217,7 +230,7 @@ func (c *external) getPrivileges(ctx context.Context, username, dbname string, t return ret, nil, nil } -func (c *external) parseGrantRows(ctx context.Context, username string, host string, dbname string, table string) ([]string, error) { +func (c *external) parseGrantRows(ctx context.Context, username, host, dbname, table string) ([]string, error) { query := fmt.Sprintf("SHOW GRANTS FOR %s@%s", mysql.QuoteValue(username), mysql.QuoteValue(host)) rows, err := c.db.Query(ctx, xsql.Query{String: query}) @@ -254,15 +267,14 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalCreation{}, errors.New(errNotGrant) } - username := *cr.Spec.ForProvider.User + username, host := mysql.SplitUserHost(*cr.Spec.ForProvider.User) dbname := defaultIdentifier(cr.Spec.ForProvider.Database) table := defaultIdentifier(cr.Spec.ForProvider.Table) privileges, grantOption := getPrivilegesString(cr.Spec.ForProvider.Privileges.ToStringSlice()) - binlog := cr.Spec.ForProvider.BinLog - query := createGrantQuery(privileges, dbname, username, table, grantOption) + query := createGrantQuery(privileges, dbname, username, host, table, grantOption) - if err := mysql.ExecWithBinlogAndFlush(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errCreateGrant}, mysql.ExecOptions{Binlog: binlog}); err != nil { + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errCreateGrant}); err != nil { return managed.ExternalCreation{}, err } return managed.ExternalCreation{}, nil @@ -274,10 +286,9 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalUpdate{}, errors.New(errNotGrant) } - username := *cr.Spec.ForProvider.User + username, host := mysql.SplitUserHost(*cr.Spec.ForProvider.User) dbname := defaultIdentifier(cr.Spec.ForProvider.Database) table := defaultIdentifier(cr.Spec.ForProvider.Table) - binlog := cr.Spec.ForProvider.BinLog observed := cr.Status.AtProvider.Privileges desired := cr.Spec.ForProvider.Privileges.ToStringSlice() @@ -286,12 +297,11 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext if len(toRevoke) > 0 { sort.Strings(toRevoke) privileges, grantOption := getPrivilegesString(toRevoke) - query := createRevokeQuery(privileges, dbname, username, table, grantOption) - if err := mysql.ExecWithBinlogAndFlush(ctx, c.db, + query := createRevokeQuery(privileges, dbname, username, host, table, grantOption) + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{ Query: query, ErrorValue: errRevokeGrant, - }, mysql.ExecOptions{ - Binlog: binlog}); err != nil { + }); err != nil { return managed.ExternalUpdate{}, err } } @@ -299,12 +309,11 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext if len(toGrant) > 0 { sort.Strings(toGrant) privileges, grantOption := getPrivilegesString(toGrant) - query := createGrantQuery(privileges, dbname, username, table, grantOption) - if err := mysql.ExecWithBinlogAndFlush(ctx, c.db, + query := createGrantQuery(privileges, dbname, username, host, table, grantOption) + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{ Query: query, ErrorValue: errCreateGrant, - }, mysql.ExecOptions{ - Binlog: binlog}); err != nil { + }); err != nil { return managed.ExternalUpdate{}, err } } @@ -326,8 +335,7 @@ func getPrivilegesString(privileges []string) (string, bool) { return out, grantOption } -func createRevokeQuery(privileges, dbname, username string, table string, grantOption bool) string { - username, host := mysql.SplitUserHost(username) +func createRevokeQuery(privileges, dbname, username, host, table string, grantOption bool) string { result := fmt.Sprintf("REVOKE %s ON %s.%s FROM %s@%s", privileges, dbname, @@ -343,8 +351,7 @@ func createRevokeQuery(privileges, dbname, username string, table string, grantO return result } -func createGrantQuery(privileges, dbname, username string, table string, grantOption bool) string { - username, host := mysql.SplitUserHost(username) +func createGrantQuery(privileges, dbname, username, host, table string, grantOption bool) string { result := fmt.Sprintf("GRANT %s ON %s.%s TO %s@%s", privileges, dbname, @@ -366,15 +373,14 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { return errors.New(errNotGrant) } - username := *cr.Spec.ForProvider.User + username, host := mysql.SplitUserHost(*cr.Spec.ForProvider.User) dbname := defaultIdentifier(cr.Spec.ForProvider.Database) table := defaultIdentifier(cr.Spec.ForProvider.Table) - binlog := cr.Spec.ForProvider.BinLog privileges, grantOption := getPrivilegesString(cr.Spec.ForProvider.Privileges.ToStringSlice()) - query := createRevokeQuery(privileges, dbname, username, table, grantOption) + query := createRevokeQuery(privileges, dbname, username, host, table, grantOption) - if err := mysql.ExecWithBinlogAndFlush(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errRevokeGrant}, mysql.ExecOptions{Binlog: binlog}); err != nil { + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errRevokeGrant}); err != nil { var myErr *mysqldriver.MySQLError if errors.As(err, &myErr) && myErr.Number == errCodeNoSuchGrant { // MySQL automatically deletes related grants if the user has been deleted diff --git a/pkg/controller/mysql/grant/reconciler_test.go b/pkg/controller/mysql/grant/reconciler_test.go index 2eefa995..49b2eaf4 100644 --- a/pkg/controller/mysql/grant/reconciler_test.go +++ b/pkg/controller/mysql/grant/reconciler_test.go @@ -74,7 +74,7 @@ func TestConnect(t *testing.T) { type fields struct { kube client.Client usage resource.Tracker - newDB func(creds map[string][]byte, tls *string) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB } type args struct { @@ -1230,11 +1230,11 @@ func Test_diffPermissions(t *testing.T) { } for name, tc := range cases { t.Run(name, func(t *testing.T) { - gotToGrant, gotToRevoke := diffPermissions(tc.args.desired, tc.args.observed) - if diff := cmp.Diff(tc.want.toGrant, gotToGrant, equateSlices()...); diff != "" { + gotToGrant, gotToRevoke := diffPermissions(tc.desired, tc.observed) + if diff := cmp.Diff(tc.toGrant, gotToGrant, equateSlices()...); diff != "" { t.Errorf("\ndiffPermissions(...): -want toGrant, +got toGrant:\n%s", diff) } - if diff := cmp.Diff(tc.want.toRevoke, gotToRevoke, equateSlices()...); diff != "" { + if diff := cmp.Diff(tc.toRevoke, gotToRevoke, equateSlices()...); diff != "" { t.Errorf("\ndiffPermissions(...): -want toRevoke, +got toRevoke:\n%s", diff) } }) diff --git a/pkg/controller/mysql/tls/tls.go b/pkg/controller/mysql/tls/tls.go new file mode 100644 index 00000000..f4a6315a --- /dev/null +++ b/pkg/controller/mysql/tls/tls.go @@ -0,0 +1,112 @@ +package tls + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + + "github.com/crossplane-contrib/provider-sql/apis/mysql/v1alpha1" + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/go-sql-driver/mysql" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// LoadConfig loads the TLS configuration when tls mode is set to custom and +// returns the tls name of registered configuration. +func LoadConfig(ctx context.Context, kube client.Client, providerConfigName string, mode *string, cfg *v1alpha1.TLSConfig) (*string, error) { + if mode == nil || *mode != "custom" { + if cfg != nil { + return nil, fmt.Errorf("tlsConfig is allowed only when tls=custom") + } + return mode, nil + } + + if err := validateTLSConfig(cfg); err != nil { + return nil, err + } + + tlsName := fmt.Sprintf("custom-%s", providerConfigName) + err := registerTLS(ctx, kube, tlsName, cfg) + if err != nil { + return nil, err + } + return &tlsName, nil +} + +func validateTLSConfig(cfg *v1alpha1.TLSConfig) error { + if cfg == nil || + cfg.CACert.SecretRef.Name == "" || + cfg.CACert.SecretRef.Key == "" || + cfg.ClientCert.SecretRef.Name == "" || + cfg.ClientCert.SecretRef.Key == "" || + cfg.ClientKey.SecretRef.Name == "" || + cfg.ClientKey.SecretRef.Key == "" { + return fmt.Errorf("tlsConfig is required when tls=custom") + } + return nil +} + +func registerTLS(ctx context.Context, kube client.Client, tlsName string, cfg *v1alpha1.TLSConfig) error { + if cfg == nil { + return nil + } + + caCert, err := getSecret(ctx, kube, cfg.CACert.SecretRef) + if err != nil { + return fmt.Errorf("cannot get CA certificate: %w", err) + } + + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(caCert); !ok { + return fmt.Errorf("cannot append CA certificate to pool") + } + + keyPair, err := getClientKeyPair(ctx, kube, cfg) + if err != nil { + return err + } + + return mysql.RegisterTLSConfig(tlsName, &tls.Config{ + RootCAs: pool, + Certificates: []tls.Certificate{keyPair}, + InsecureSkipVerify: cfg.InsecureSkipVerify, //nolint:gosec // This is only required by integration tests and should never be used in production + }) +} + +func getClientKeyPair(ctx context.Context, kube client.Client, cfg *v1alpha1.TLSConfig) (tls.Certificate, error) { + cert, err := getSecret(ctx, kube, cfg.ClientCert.SecretRef) + if err != nil { + return tls.Certificate{}, fmt.Errorf("cannot get client certificate: %w", err) + } + + key, err := getSecret(ctx, kube, cfg.ClientKey.SecretRef) + if err != nil { + return tls.Certificate{}, fmt.Errorf("cannot get client key: %w", err) + } + + keyPair, err := tls.X509KeyPair(cert, key) + if err != nil { + return tls.Certificate{}, errors.Wrap(err, "cannot make client certificate") + } + return keyPair, nil +} + +func getSecret(ctx context.Context, kube client.Client, sel xpv1.SecretKeySelector) ([]byte, error) { + secret := &corev1.Secret{} + if err := kube.Get(ctx, types.NamespacedName{ + Namespace: sel.Namespace, + Name: sel.Name}, secret); err != nil { + return nil, fmt.Errorf("cannot get Secret %q in namespace %q: %w", sel.Name, sel.Namespace, err) + } + + data, ok := secret.Data[sel.Key] + if !ok { + return nil, fmt.Errorf("key %q not found in Secret %q", sel.Key, sel.Name) + } + + return data, nil +} diff --git a/pkg/controller/mysql/user/reconciler.go b/pkg/controller/mysql/user/reconciler.go index b446e2bb..07de47d7 100644 --- a/pkg/controller/mysql/user/reconciler.go +++ b/pkg/controller/mysql/user/reconciler.go @@ -31,6 +31,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/feature" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/password" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" @@ -39,6 +40,7 @@ import ( "github.com/crossplane-contrib/provider-sql/apis/mysql/v1alpha1" "github.com/crossplane-contrib/provider-sql/pkg/clients/mysql" "github.com/crossplane-contrib/provider-sql/pkg/clients/xsql" + "github.com/crossplane-contrib/provider-sql/pkg/controller/mysql/tls" ) const ( @@ -46,6 +48,7 @@ const ( errGetPC = "cannot get ProviderConfig" errNoSecretRef = "ProviderConfig does not reference a credentials Secret" errGetSecret = "cannot get credentials Secret" + errTLSConfig = "cannot load TLS config" errNotUser = "managed resource is not a User custom resource" errSelectUser = "cannot select user" @@ -63,13 +66,19 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { name := managed.ControllerName(v1alpha1.UserGroupKind) t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.UserGroupVersionKind), + reconcilerOptions := []managed.ReconcilerOption{ managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: mysql.New}), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) - + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.UserGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.User{}). @@ -82,7 +91,7 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { type connector struct { kube client.Client usage resource.Tracker - newDB func(creds map[string][]byte, tls *string) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB } func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { @@ -97,8 +106,9 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E // ProviderConfigReference could theoretically be nil, but in practice the // DefaultProviderConfig initializer will set it before we get here. + providerConfigName := cr.GetProviderConfigReference().Name pc := &v1alpha1.ProviderConfig{} - if err := c.kube.Get(ctx, types.NamespacedName{Name: cr.GetProviderConfigReference().Name}, pc); err != nil { + if err := c.kube.Get(ctx, types.NamespacedName{Name: providerConfigName}, pc); err != nil { return nil, errors.Wrap(err, errGetPC) } @@ -115,8 +125,13 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E return nil, errors.Wrap(err, errGetSecret) } + tlsName, err := tls.LoadConfig(ctx, c.kube, providerConfigName, pc.Spec.TLS, pc.Spec.TLSConfig) + if err != nil { + return nil, errors.Wrap(err, errTLSConfig) + } + return &external{ - db: c.newDB(s.Data, pc.Spec.TLS), + db: c.newDB(s.Data, tlsName, cr.Spec.ForProvider.BinLog), kube: c.kube, }, nil } @@ -251,8 +266,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext } ro := resourceOptionsToClauses(cr.Spec.ForProvider.ResourceOptions) - binlog := cr.Spec.ForProvider.BinLog - if err := c.executeCreateUserQuery(ctx, username, host, ro, pw, binlog); err != nil { + if err := c.executeCreateUserQuery(ctx, username, host, ro, pw); err != nil { return managed.ExternalCreation{}, err } @@ -265,7 +279,7 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext }, nil } -func (c *external) executeCreateUserQuery(ctx context.Context, username string, host string, resourceOptionsClauses []string, pw string, binlog *bool) error { +func (c *external) executeCreateUserQuery(ctx context.Context, username string, host string, resourceOptionsClauses []string, pw string) error { resourceOptions := "" if len(resourceOptionsClauses) != 0 { resourceOptions = fmt.Sprintf(" WITH %s", strings.Join(resourceOptionsClauses, " ")) @@ -279,7 +293,7 @@ func (c *external) executeCreateUserQuery(ctx context.Context, username string, resourceOptions, ) - if err := mysql.ExecWithBinlogAndFlush(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errCreateUser}, mysql.ExecOptions{Binlog: binlog}); err != nil { + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errCreateUser}); err != nil { return err } @@ -303,14 +317,13 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext if len(rochanged) > 0 { resourceOptions := fmt.Sprintf("WITH %s", strings.Join(ro, " ")) - binlog := cr.Spec.ForProvider.BinLog query := fmt.Sprintf( "ALTER USER %s@%s %s", mysql.QuoteValue(username), mysql.QuoteValue(host), resourceOptions, ) - if err := mysql.ExecWithBinlogAndFlush(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}, mysql.ExecOptions{Binlog: binlog}); err != nil { + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}); err != nil { return managed.ExternalUpdate{}, err } @@ -336,9 +349,8 @@ func (c *external) UpdatePassword(ctx context.Context, cr *v1alpha1.User, userna } if pwchanged { - binlog := cr.Spec.ForProvider.BinLog query := fmt.Sprintf("ALTER USER %s@%s IDENTIFIED BY %s", mysql.QuoteValue(username), mysql.QuoteValue(host), mysql.QuoteValue(pw)) - if err := mysql.ExecWithBinlogAndFlush(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}, mysql.ExecOptions{Binlog: binlog}); err != nil { + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errUpdateUser}); err != nil { return managed.ConnectionDetails{}, err } @@ -358,9 +370,8 @@ func (c *external) Delete(ctx context.Context, mg resource.Managed) error { username, host := mysql.SplitUserHost(meta.GetExternalName(cr)) - binlog := cr.Spec.ForProvider.BinLog query := fmt.Sprintf("DROP USER IF EXISTS %s@%s", mysql.QuoteValue(username), mysql.QuoteValue(host)) - if err := mysql.ExecWithBinlogAndFlush(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errDropUser}, mysql.ExecOptions{Binlog: binlog}); err != nil { + if err := mysql.ExecWrapper(ctx, c.db, mysql.ExecQuery{Query: query, ErrorValue: errDropUser}); err != nil { return err } diff --git a/pkg/controller/mysql/user/reconciler_test.go b/pkg/controller/mysql/user/reconciler_test.go index 17858ccf..855fe457 100644 --- a/pkg/controller/mysql/user/reconciler_test.go +++ b/pkg/controller/mysql/user/reconciler_test.go @@ -72,7 +72,7 @@ func TestConnect(t *testing.T) { type fields struct { kube client.Client usage resource.Tracker - newDB func(creds map[string][]byte, tls *string) xsql.DB + newDB func(creds map[string][]byte, tls *string, binlog *bool) xsql.DB } type args struct { diff --git a/pkg/controller/postgresql/database/reconciler.go b/pkg/controller/postgresql/database/reconciler.go index c120f50f..9990a5f7 100644 --- a/pkg/controller/postgresql/database/reconciler.go +++ b/pkg/controller/postgresql/database/reconciler.go @@ -34,6 +34,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/feature" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" @@ -67,13 +68,19 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { name := managed.ControllerName(v1alpha1.DatabaseGroupKind) t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.DatabaseGroupVersionKind), + reconcilerOptions := []managed.ReconcilerOption{ managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) - + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.DatabaseGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.Database{}). diff --git a/pkg/controller/postgresql/extension/reconciler.go b/pkg/controller/postgresql/extension/reconciler.go index 6d82168d..ac2ae11d 100644 --- a/pkg/controller/postgresql/extension/reconciler.go +++ b/pkg/controller/postgresql/extension/reconciler.go @@ -31,6 +31,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/feature" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" @@ -59,13 +60,19 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { name := managed.ControllerName(v1alpha1.ExtensionGroupKind) t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.ExtensionGroupVersionKind), + reconcilerOptions := []managed.ReconcilerOption{ managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) - + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.ExtensionGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.Extension{}). diff --git a/pkg/controller/postgresql/grant/reconciler.go b/pkg/controller/postgresql/grant/reconciler.go index 85d0175c..7a236c09 100644 --- a/pkg/controller/postgresql/grant/reconciler.go +++ b/pkg/controller/postgresql/grant/reconciler.go @@ -32,6 +32,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/feature" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" @@ -68,13 +69,19 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { name := managed.ControllerName(v1alpha1.GrantGroupKind) t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.GrantGroupVersionKind), + reconcilerOptions := []managed.ReconcilerOption{ managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) - + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.GrantGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.Grant{}). @@ -276,6 +283,14 @@ func createGrantQueries(gp v1alpha1.GrantParameters, ql *[]xsql.Query) error { / withOption(gp.WithOption), )}, ) + if gp.RevokePublicOnDb != nil && *gp.RevokePublicOnDb { + *ql = append(*ql, + // REVOKE FROM PUBLIC + xsql.Query{String: fmt.Sprintf("REVOKE ALL ON DATABASE %s FROM PUBLIC", + db, + )}, + ) + } return nil } return errors.New(errUnknownGrant) diff --git a/pkg/controller/postgresql/role/reconciler.go b/pkg/controller/postgresql/role/reconciler.go index 474c93a8..055e4b77 100644 --- a/pkg/controller/postgresql/role/reconciler.go +++ b/pkg/controller/postgresql/role/reconciler.go @@ -35,6 +35,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/feature" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/password" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" @@ -69,13 +70,19 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { name := managed.ControllerName(v1alpha1.RoleGroupKind) t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.RoleGroupVersionKind), + reconcilerOptions := []managed.ReconcilerOption{ managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) - + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.RoleGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.Role{}). diff --git a/pkg/controller/postgresql/schema/reconciler.go b/pkg/controller/postgresql/schema/reconciler.go index 6209f60b..d60cbe13 100644 --- a/pkg/controller/postgresql/schema/reconciler.go +++ b/pkg/controller/postgresql/schema/reconciler.go @@ -31,6 +31,7 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" xpcontroller "github.com/crossplane/crossplane-runtime/pkg/controller" "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/feature" "github.com/crossplane/crossplane-runtime/pkg/meta" "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" "github.com/crossplane/crossplane-runtime/pkg/resource" @@ -62,13 +63,19 @@ func Setup(mgr ctrl.Manager, o xpcontroller.Options) error { name := managed.ControllerName(v1alpha1.SchemaGroupKind) t := resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1alpha1.ProviderConfigUsage{}) - r := managed.NewReconciler(mgr, - resource.ManagedKind(v1alpha1.SchemaGroupVersionKind), + reconcilerOptions := []managed.ReconcilerOption{ managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), usage: t, newDB: postgresql.New}), managed.WithLogger(o.Logger.WithValues("controller", name)), managed.WithPollInterval(o.PollInterval), - managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name)))) - + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + } + if o.Features.Enabled(feature.EnableBetaManagementPolicies) { + reconcilerOptions = append(reconcilerOptions, managed.WithManagementPolicies()) + } + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.SchemaGroupVersionKind), + reconcilerOptions..., + ) return ctrl.NewControllerManagedBy(mgr). Named(name). For(&v1alpha1.Schema{}). @@ -167,16 +174,14 @@ func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalCreation{}, errors.New(errNotSchema) } - var b strings.Builder - b.WriteString("CREATE SCHEMA IF NOT EXISTS ") - b.WriteString(pq.QuoteIdentifier(meta.GetExternalName(cr))) + var queries []xsql.Query - if cr.Spec.ForProvider.Role != nil { - b.WriteString(" AUTHORIZATION ") - b.WriteString(pq.QuoteIdentifier(*cr.Spec.ForProvider.Role)) - } + cr.SetConditions(xpv1.Creating()) + + createSchemaQueries(cr.Spec.ForProvider, &queries, meta.GetExternalName(cr)) - return managed.ExternalCreation{}, errors.Wrap(c.db.Exec(ctx, xsql.Query{String: b.String()}), errCreateSchema) + err := c.db.ExecTx(ctx, queries) + return managed.ExternalCreation{}, errors.Wrap(err, errCreateSchema) } func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { //nolint:gocyclo @@ -189,13 +194,10 @@ func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.Ext return managed.ExternalUpdate{}, nil } - var b strings.Builder - b.WriteString("ALTER SCHEMA ") - b.WriteString(pq.QuoteIdentifier(meta.GetExternalName(mg))) - b.WriteString(" OWNER TO ") - b.WriteString(pq.QuoteIdentifier(*cr.Spec.ForProvider.Role)) + var queries []xsql.Query + updateSchemaQueries(cr.Spec.ForProvider, &queries, meta.GetExternalName(cr)) - err := c.db.Exec(ctx, xsql.Query{String: b.String()}) + err := c.db.ExecTx(ctx, queries) return managed.ExternalUpdate{}, errors.Wrap(err, errAlterSchema) } @@ -226,3 +228,46 @@ func lateInit(observed v1alpha1.SchemaParameters, desired *v1alpha1.SchemaParame return li } + +func createSchemaQueries(sp v1alpha1.SchemaParameters, ql *[]xsql.Query, en string) { // nolint: gocyclo + + var b strings.Builder + b.WriteString("CREATE SCHEMA IF NOT EXISTS ") + b.WriteString(pq.QuoteIdentifier(en)) + + if sp.Role != nil { + b.WriteString(" AUTHORIZATION ") + b.WriteString(pq.QuoteIdentifier(*sp.Role)) + b.WriteString(";") + } + + *ql = append(*ql, + xsql.Query{String: b.String()}, + ) + + if sp.RevokePublicOnSchema != nil && *sp.RevokePublicOnSchema { + *ql = append(*ql, + xsql.Query{String: "REVOKE ALL ON SCHEMA PUBLIC FROM PUBLIC;"}, + ) + } + +} + +func updateSchemaQueries(sp v1alpha1.SchemaParameters, ql *[]xsql.Query, en string) { // nolint: gocyclo + + var b strings.Builder + b.WriteString("ALTER SCHEMA ") + b.WriteString(pq.QuoteIdentifier(en)) + b.WriteString(" OWNER TO ") + b.WriteString(pq.QuoteIdentifier(*sp.Role)) + + *ql = append(*ql, + xsql.Query{String: b.String()}, + ) + + if sp.RevokePublicOnSchema != nil && *sp.RevokePublicOnSchema { + *ql = append(*ql, + xsql.Query{String: "REVOKE ALL ON SCHEMA PUBLIC FROM PUBLIC;"}, + ) + } +} diff --git a/pkg/controller/postgresql/schema/reconciler_test.go b/pkg/controller/postgresql/schema/reconciler_test.go index 8423ef4b..d2c493e5 100644 --- a/pkg/controller/postgresql/schema/reconciler_test.go +++ b/pkg/controller/postgresql/schema/reconciler_test.go @@ -371,7 +371,7 @@ func TestCreate(t *testing.T) { reason: "Any errors encountered while creating the schema should be returned", fields: fields{ db: &mockDB{ - MockExec: func(ctx context.Context, q xsql.Query) error { return errBoom }, + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { return errBoom }, }, }, args: args{ @@ -387,7 +387,7 @@ func TestCreate(t *testing.T) { reason: "No error should be returned when we successfully create a extension", fields: fields{ db: &mockDB{ - MockExec: func(ctx context.Context, q xsql.Query) error { return nil }, + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { return nil }, }, }, args: args{ @@ -457,7 +457,7 @@ func TestUpdate(t *testing.T) { reason: "No error should be returned when we successfully update a schema", fields: fields{ db: &mockDB{ - MockExec: func(ctx context.Context, q xsql.Query) error { return nil }, + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { return nil }, }, }, args: args{