From 0156e42f5583de12b2bc3705b0f7f32e255beaac Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Wed, 10 Apr 2024 13:28:15 +0200 Subject: [PATCH 01/26] feat(k8s): more boilerplate: add GitHub actions, license, PDOK golangci-lint config, upgrade to Go 1.22 --- .github/workflows/build-and-publish-image.yml | 78 +++++++++++ .github/workflows/lint-go.yml | 28 ++++ .github/workflows/test-go.yml | 39 ++++++ .golangci.yml | 124 +++++++++++++----- .yamllint | 14 ++ Dockerfile | 4 +- LICENSE | 21 +++ go.mod | 2 +- hack/boilerplate.go.txt | 28 ++-- 9 files changed, 294 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/build-and-publish-image.yml create mode 100644 .github/workflows/lint-go.yml create mode 100644 .github/workflows/test-go.yml create mode 100644 .yamllint create mode 100644 LICENSE diff --git a/.github/workflows/build-and-publish-image.yml b/.github/workflows/build-and-publish-image.yml new file mode 100644 index 0000000..0292c84 --- /dev/null +++ b/.github/workflows/build-and-publish-image.yml @@ -0,0 +1,78 @@ +--- +name: build +env: + image: pdok/uptime-operator +on: + push: + tags: + - '*' +jobs: + docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.22' + - name: Make test + run: | + make test + echo "removing generated code from coverage results" + diffs="$(git status -s)" + if [[ -n "$diffs" ]]; then echo "there are diffs after make test: $diffs"; exit 250; fi + - name: Docker meta + id: docker_meta + uses: docker/metadata-action@v3 + with: + images: ${{ env.image }} + tags: | + type=semver,pattern={{major}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{version}} + - name: Login to PDOK Docker Hub + if: startsWith(env.image, 'pdok/') + uses: docker/login-action@v1 + with: + username: koalapdok + password: ${{ secrets.DOCKERHUB_PUSH }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + push: true + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new + - # Temp fix to cleanup cache + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + - name: Build result notification + if: success() || failure() + uses: 8398a7/action-slack@v3 + with: + fields: all + status: custom + custom_payload: | + { + attachments: [{ + color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning', + text: `${process.env.AS_WORKFLOW} ${{ job.status }} for ${process.env.AS_REPO}!\n${process.env.AS_JOB} job on ${process.env.AS_REF} (commit: ${process.env.AS_COMMIT}, version: ${{ steps.docker_meta.outputs.version }}) by ${process.env.AS_AUTHOR} took ${process.env.AS_TOOK}`, + }] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/lint-go.yml b/.github/workflows/lint-go.yml new file mode 100644 index 0000000..01924a1 --- /dev/null +++ b/.github/workflows/lint-go.yml @@ -0,0 +1,28 @@ +--- +name: lint (go) +on: + push: + branches: + - master + pull_request: +permissions: + contents: read +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v4 + with: + go-version: '1.22' + cache: false + + - uses: actions/checkout@v3 + + - name: tidy + uses: katexochen/go-tidy-check@v2 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml new file mode 100644 index 0000000..3bf5e2b --- /dev/null +++ b/.github/workflows/test-go.yml @@ -0,0 +1,39 @@ +--- +name: test (go) +on: + push: + branches: + - master + pull_request: +permissions: + contents: write +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.22' + + - name: Make test + run: | + make test + echo "removing generated code from coverage results" + mv cover.out cover.out.tmp && grep -vP "uptime-operator/(api/v1alpha1|cmd|test/utils)/" cover.out.tmp > cover.out + diffs="$(git status -s)" + if [[ -n "$diffs" ]]; then echo "there are diffs after make test: $diffs"; exit 250; fi + + - name: Update coverage report + uses: ncruces/go-coverage-report@v0 + with: + coverage-file: cover.out + report: true + chart: false + amend: false + reuse-go: true + if: | + github.event_name == 'push' + continue-on-error: false diff --git a/.golangci.yml b/.golangci.yml index aed8644..e643524 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,40 +1,102 @@ +--- run: - deadline: 5m - allow-parallel-runners: true + # Timeout for analysis. + timeout: 5m + + # Modules download mode (do not modify go.mod) + module-download-mode: readonly + + # Include test files (see below to exclude certain linters) + tests: true issues: - # don't skip warning about doc comments - # don't exclude the default set of lint - exclude-use-default: false - # restore some of the defaults - # (fill in the rest as needed) exclude-rules: - - path: "api/*" - linters: - - lll - - path: "internal/*" + # Exclude certain linters for test code + - path: "_test\\.go" linters: + - bodyclose - dupl - - lll + - funlen + +output: + format: colored-line-number + print-issued-lines: true + print-linter-name: true + +linters-settings: + depguard: + rules: + main: + # Packages that are not allowed where the value is a suggestion. + deny: + - pkg: "github.com/pkg/errors" + desc: Should be replaced by standard lib errors package + cyclop: + # The maximal code complexity to report. + max-complexity: 15 + skip-tests: true + funlen: + lines: 100 + gomoddirectives: + replace-allow-list: + - github.com/abbot/go-http-auth # https://github.com/traefik/traefik/issues/6873#issuecomment-637654361 + nestif: + min-complexity: 6 + linters: disable-all: true enable: - - dupl - - errcheck - - exportloopref - - goconst - - gocyclo - - gofmt - - goimports - - gosimple - - govet - - ineffassign - - lll - - misspell - - nakedret - - prealloc - - staticcheck - - typecheck - - unconvert - - unparam - - unused + # enabled by default by golangci-lint + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - gosimple # specializes in simplifying a code + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - typecheck # like the front-end of a Go compiler, parses and type-checks Go code + - unused # checks for unused constants, variables, functions and types + # extra enabled by us + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - cyclop # checks function and package cyclomatic complexity + - dupl # tool for code clone detection + - durationcheck # checks for two durations multiplied together + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - execinquery # checks query string in Query function which reads your Go src files and warning it finds + - exhaustive # checks exhaustiveness of enum switch statements + - exportloopref # checks for pointers to enclosing loop variables + - forbidigo # forbids identifiers + - funlen # tool for detection of long functions + - gocheckcompilerdirectives # validates go compiler directive comments (//go:) + - goconst # finds repeated strings that could be replaced by a constant + - gocritic # provides diagnostics that check for bugs, performance and style issues + - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt + - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) + - makezero # finds slice declarations with non-zero initial length + - nakedret # finds naked returns in functions greater than a specified function length + - nestif # reports deeply nested if statements + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - nolintlint # reports ill-formed or insufficient nolint directives + - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - perfsprint # Golang linter for performance, aiming at usages of fmt.Sprintf which have faster alternatives + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter # checks Prometheus metrics naming via promlint + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - rowserrcheck # checks whether Err of rows is checked successfully + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - sloglint # A Go linter that ensures consistent code style when using log/slog + - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 + - testableexamples # checks if examples are testable (have an expected output) + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + - wastedassign # finds wasted assignment statements + fast: false diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..f079484 --- /dev/null +++ b/.yamllint @@ -0,0 +1,14 @@ +extends: default + +ignore: | + .golangci.yaml + +# (deduced from generated yaml by kubebuilder:) +rules: + comments: + require-starting-space: false + document-start: false + indentation: + indent-sequences: consistent + line-length: + max: 120 diff --git a/Dockerfile b/Dockerfile index aca26f9..8ed9335 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.21 AS builder +FROM golang:1.22 AS builder ARG TARGETOS ARG TARGETARCH @@ -21,7 +21,7 @@ COPY internal/controller/ internal/controller/ # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} go build -a -o manager cmd/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..398a5f4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/go.mod b/go.mod index 5eb860f..02a163d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/PDOK/uptime-operator -go 1.21 +go 1.22 require ( github.com/onsi/ginkgo/v2 v2.14.0 diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index 77f766e..02c63b7 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,15 +1,23 @@ /* -Copyright 2024 pdok.nl. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ \ No newline at end of file From f60d3e4558911fd53c9dc0eee895d8a36d10166d Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Wed, 10 Apr 2024 13:54:34 +0200 Subject: [PATCH 02/26] feat(k8s): fix tests --- .github/workflows/test-go.yml | 4 --- test/e2e/e2e_suite_test.go | 34 +++++++++++++++---------- test/e2e/e2e_test.go | 47 +++++++++++++++++++++-------------- test/utils/utils.go | 38 +++++++++++++++++----------- 4 files changed, 72 insertions(+), 51 deletions(-) diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml index 3bf5e2b..e57035f 100644 --- a/.github/workflows/test-go.yml +++ b/.github/workflows/test-go.yml @@ -21,10 +21,6 @@ jobs: - name: Make test run: | make test - echo "removing generated code from coverage results" - mv cover.out cover.out.tmp && grep -vP "uptime-operator/(api/v1alpha1|cmd|test/utils)/" cover.out.tmp > cover.out - diffs="$(git status -s)" - if [[ -n "$diffs" ]]; then echo "there are diffs after make test: $diffs"; exit 250; fi - name: Update coverage report uses: ncruces/go-coverage-report@v0 diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index fb55370..041a6c9 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -1,17 +1,25 @@ /* -Copyright 2024 pdok.nl. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package e2e @@ -20,13 +28,13 @@ import ( "fmt" "testing" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd + . "github.com/onsi/gomega" //nolint:revive // ginkgo bdd ) // Run e2e tests using the Ginkgo runner. func TestE2E(t *testing.T) { RegisterFailHandler(Fail) - fmt.Fprintf(GinkgoWriter, "Starting uptime-operator suite\n") + fmt.Fprintf(GinkgoWriter, "Starting ogcapi-operator suite\n") RunSpecs(t, "e2e suite") } diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index b877b9d..7d1079d 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -1,17 +1,25 @@ /* -Copyright 2024 pdok.nl. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package e2e @@ -21,13 +29,13 @@ import ( "os/exec" "time" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd + . "github.com/onsi/gomega" //nolint:revive // ginkgo bdd "github.com/PDOK/uptime-operator/test/utils" ) -const namespace = "uptime-operator-system" +const namespace = "ogcapi-operator-system" var _ = Describe("controller", Ordered, func() { BeforeAll(func() { @@ -60,10 +68,10 @@ var _ = Describe("controller", Ordered, func() { var err error // projectimage stores the name of the image used in the example - var projectimage = "example.com/uptime-operator:v0.0.1" + var projectimage = "example.com/ogcapi-operator:v0.0.1" By("building the manager(Operator) image") - cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectimage)) + cmd := exec.Command("make", "docker-build", "IMG="+projectimage) _, err = utils.Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred()) @@ -74,9 +82,10 @@ var _ = Describe("controller", Ordered, func() { By("installing CRDs") cmd = exec.Command("make", "install") _, err = utils.Run(cmd) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) By("deploying the controller-manager") - cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectimage)) + cmd = exec.Command("make", "deploy", "IMG=%s"+projectimage) _, err = utils.Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred()) diff --git a/test/utils/utils.go b/test/utils/utils.go index 0398412..5effe37 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -1,17 +1,25 @@ /* -Copyright 2024 pdok.nl. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package utils @@ -60,7 +68,7 @@ func Run(cmd *exec.Cmd) ([]byte, error) { fmt.Fprintf(GinkgoWriter, "running: %s\n", command) output, err := cmd.CombinedOutput() if err != nil { - return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) + return output, fmt.Errorf("%s failed with error: (%w) %s", command, err, string(output)) } return output, nil @@ -135,6 +143,6 @@ func GetProjectDir() (string, error) { if err != nil { return wd, err } - wd = strings.Replace(wd, "/test/e2e", "", -1) + wd = strings.ReplaceAll(wd, "/test/e2e", "") return wd, nil } From da18f55caacdda51f9b72e20cca79d517bbf4bc0 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Wed, 10 Apr 2024 16:59:09 +0200 Subject: [PATCH 03/26] feat(controller): generate controller to watch Traefik ingress routes. kubebuilder create api --group traefik.containo.us --version v1alpha1 --kind IngressRoute --resource=false --controller=true --- PROJECT | 6 ++ cmd/main.go | 9 ++ .../controller/ingressroute_controller.go | 69 ++++++++++++++ .../ingressroute_controller_test.go | 40 ++++++++ internal/controller/suite_test.go | 93 +++++++++++++++++++ 5 files changed, 217 insertions(+) create mode 100644 internal/controller/ingressroute_controller.go create mode 100644 internal/controller/ingressroute_controller_test.go create mode 100644 internal/controller/suite_test.go diff --git a/PROJECT b/PROJECT index 6b3e951..37fcfa1 100644 --- a/PROJECT +++ b/PROJECT @@ -7,4 +7,10 @@ layout: - go.kubebuilder.io/v4 projectName: uptime-operator repo: github.com/PDOK/uptime-operator +resources: +- controller: true + domain: pdok.nl + group: traefik.containo.us + kind: IngressRoute + version: v1alpha1 version: "3" diff --git a/cmd/main.go b/cmd/main.go index 7c8fcc4..f80262d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -33,6 +33,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/PDOK/uptime-operator/internal/controller" //+kubebuilder:scaffold:imports ) @@ -118,6 +120,13 @@ func main() { os.Exit(1) } + if err = (&controller.IngressRouteReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "IngressRoute") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/internal/controller/ingressroute_controller.go b/internal/controller/ingressroute_controller.go new file mode 100644 index 0000000..77743fa --- /dev/null +++ b/internal/controller/ingressroute_controller.go @@ -0,0 +1,69 @@ +/* +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package controller + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// IngressRouteReconciler reconciles a IngressRoute object +type IngressRouteReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=traefik.containo.us.pdok.nl,resources=ingressroutes,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=traefik.containo.us.pdok.nl,resources=ingressroutes/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=traefik.containo.us.pdok.nl,resources=ingressroutes/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the IngressRoute object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile +func (r *IngressRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *IngressRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + // Uncomment the following line adding a pointer to an instance of the controlled resource as an argument + // For(). + Complete(r) +} diff --git a/internal/controller/ingressroute_controller_test.go b/internal/controller/ingressroute_controller_test.go new file mode 100644 index 0000000..c8e64fe --- /dev/null +++ b/internal/controller/ingressroute_controller_test.go @@ -0,0 +1,40 @@ +/* +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package controller + +import ( + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("IngressRoute Controller", func() { + Context("When reconciling a resource", func() { + + It("should successfully reconcile the resource", func() { + + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go new file mode 100644 index 0000000..5f11c74 --- /dev/null +++ b/internal/controller/suite_test.go @@ -0,0 +1,93 @@ +/* +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +package controller + +import ( + "fmt" + "path/filepath" + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", + fmt.Sprintf("1.29.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) From 509d0d43f28d4c21ef2529586bf2d12d5596fd4d Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Thu, 11 Apr 2024 14:48:55 +0200 Subject: [PATCH 04/26] feat(controller): Add hello world implementation to make sure the setup works --- PROJECT | 5 +- README.md | 35 +++-- cmd/main.go | 4 + config/rbac/role.yaml | 63 +++++++-- go.mod | 53 +++++--- go.sum | 121 +++++++++++------- .../controller/ingressroute_controller.go | 18 ++- 7 files changed, 201 insertions(+), 98 deletions(-) diff --git a/PROJECT b/PROJECT index 37fcfa1..e358f97 100644 --- a/PROJECT +++ b/PROJECT @@ -9,8 +9,11 @@ projectName: uptime-operator repo: github.com/PDOK/uptime-operator resources: - controller: true - domain: pdok.nl group: traefik.containo.us kind: IngressRoute version: v1alpha1 +- controller: true + group: traefik.io + kind: IngressRoute + version: v1alpha1 version: "3" diff --git a/README.md b/README.md index 8361be5..8e5cce7 100644 --- a/README.md +++ b/README.md @@ -98,17 +98,28 @@ More information can be found via the [Kubebuilder Documentation](https://book.k ## License -Copyright 2024 pdok.nl. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 +``` +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/cmd/main.go b/cmd/main.go index f80262d..c882bdf 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,6 +35,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" "github.com/PDOK/uptime-operator/internal/controller" + traefikcontainous "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1" + traefikiov1alpha1 "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikio/v1alpha1" //+kubebuilder:scaffold:imports ) @@ -46,6 +48,8 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(traefikcontainous.AddToScheme(scheme)) + utilruntime.Must(traefikiov1alpha1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index bc070bb..6892aa4 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -1,15 +1,58 @@ +--- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - labels: - app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: manager-role - app.kubernetes.io/component: rbac - app.kubernetes.io/created-by: uptime-operator - app.kubernetes.io/part-of: uptime-operator - app.kubernetes.io/managed-by: kustomize name: manager-role rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get", "list", "watch"] +- apiGroups: + - traefik.containo.us + resources: + - ingressroutes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - traefik.containo.us + resources: + - ingressroutes/finalizers + verbs: + - update +- apiGroups: + - traefik.containo.us + resources: + - ingressroutes/status + verbs: + - get + - patch + - update +- apiGroups: + - traefik.io + resources: + - ingressroutes + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - traefik.io + resources: + - ingressroutes/finalizers + verbs: + - update +- apiGroups: + - traefik.io + resources: + - ingressroutes/status + verbs: + - get + - patch + - update diff --git a/go.mod b/go.mod index 02a163d..6436259 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,26 @@ module github.com/PDOK/uptime-operator go 1.22 require ( - github.com/onsi/ginkgo/v2 v2.14.0 - github.com/onsi/gomega v1.30.0 - k8s.io/apimachinery v0.29.0 - k8s.io/client-go v0.29.0 - sigs.k8s.io/controller-runtime v0.17.0 + github.com/onsi/ginkgo/v2 v2.17.1 + github.com/onsi/gomega v1.32.0 + github.com/traefik/traefik/v2 v2.11.0 + k8s.io/apimachinery v0.29.3 + k8s.io/client-go v0.29.3 + sigs.k8s.io/controller-runtime v0.17.3 ) +replace github.com/abbot/go-http-auth => github.com/abbot/go-http-auth v0.4.0 // for github.com/traefik/traefik/v2 + require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.8.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-acme/lego/v4 v4.15.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect @@ -25,45 +31,52 @@ require ( github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect + github.com/google/uuid v1.4.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/miekg/dns v1.1.58 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/traefik/paerser v0.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/oauth2 v0.12.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.16.1 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.17.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.29.0 // indirect - k8s.io/apiextensions-apiserver v0.29.0 // indirect - k8s.io/component-base v0.29.0 // indirect + k8s.io/api v0.29.3 // indirect + k8s.io/apiextensions-apiserver v0.29.2 // indirect + k8s.io/component-base v0.29.2 // indirect k8s.io/klog/v2 v2.110.1 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect diff --git a/go.sum b/go.sum index 57b4fa9..48a01bf 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,16 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= @@ -17,6 +19,10 @@ github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1 github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-acme/lego/v4 v4.15.0 h1:A7MHEU3b+TDFqhC/HmzMJnzPbyeaYvMZQBbqgvbThhU= +github.com/go-acme/lego/v4 v4.15.0/go.mod h1:eeGhjW4zWT7Ccqa3sY7ayEqFLCAICx+mXgkMHKIkLxg= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -35,25 +41,25 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b h1:h9U78+dx9a4BKdQkBBos92HalKpaGKHrp+3Uo6yTodo= +github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -71,6 +77,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= +github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -78,14 +86,17 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= -github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= -github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= -github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= +github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= +github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= +github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= @@ -96,6 +107,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -103,11 +116,16 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/traefik/paerser v0.2.0 h1:zqCLGSXoNlcBd+mzqSCLjon/I6phqIjeJL2xFB2ysgQ= +github.com/traefik/paerser v0.2.0/go.mod h1:afzaVcgF8A+MpTnPG4wBr4whjanCSYA6vK5RwaYVtRc= +github.com/traefik/traefik/v2 v2.11.0 h1:Uq5fiVpcFCbAmwn/EDYmG4RoKmfw6leVPRKtW6zPF54= +github.com/traefik/traefik/v2 v2.11.0/go.mod h1:75FibnLtQVprWEC/gedCO8fZqqRT/3g6yXPzPe5kOzs= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -117,45 +135,52 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= -golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -164,10 +189,8 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -179,24 +202,24 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= -k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= -k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= -k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= -k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= -k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= -k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= -k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= -k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= -k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= +k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= +k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= +k8s.io/apiextensions-apiserver v0.29.2 h1:UK3xB5lOWSnhaCk0RFZ0LUacPZz9RY4wi/yt2Iu+btg= +k8s.io/apiextensions-apiserver v0.29.2/go.mod h1:aLfYjpA5p3OwtqNXQFkhJ56TB+spV8Gc4wfMhUA3/b8= +k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= +k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= +k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= +k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= +k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= +k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.17.0 h1:fjJQf8Ukya+VjogLO6/bNX9HE6Y2xpsO5+fyS26ur/s= -sigs.k8s.io/controller-runtime v0.17.0/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +sigs.k8s.io/controller-runtime v0.17.3 h1:65QmN7r3FWgTxDMz9fvGnO1kbf2nu+acg9p2R9oYYYk= +sigs.k8s.io/controller-runtime v0.17.3/go.mod h1:N0jpP5Lo7lMTF9aL56Z/B2oWBJjey6StQM0jRbKQXtY= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= diff --git a/internal/controller/ingressroute_controller.go b/internal/controller/ingressroute_controller.go index 77743fa..4a7bf2e 100644 --- a/internal/controller/ingressroute_controller.go +++ b/internal/controller/ingressroute_controller.go @@ -31,6 +31,8 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" + + traefikcontainous "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1" ) // IngressRouteReconciler reconciles a IngressRoute object @@ -39,9 +41,12 @@ type IngressRouteReconciler struct { Scheme *runtime.Scheme } -//+kubebuilder:rbac:groups=traefik.containo.us.pdok.nl,resources=ingressroutes,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=traefik.containo.us.pdok.nl,resources=ingressroutes/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=traefik.containo.us.pdok.nl,resources=ingressroutes/finalizers,verbs=update +//+kubebuilder:rbac:groups=traefik.containo.us,resources=ingressroutes,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=traefik.containo.us,resources=ingressroutes/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=traefik.containo.us,resources=ingressroutes/finalizers,verbs=update +//+kubebuilder:rbac:groups=traefik.io,resources=ingressroutes,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=traefik.io,resources=ingressroutes/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=traefik.io,resources=ingressroutes/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -53,7 +58,8 @@ type IngressRouteReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile func (r *IngressRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + logger := log.FromContext(ctx) + logger.Info("hello world") // TODO(user): your logic here @@ -63,7 +69,7 @@ func (r *IngressRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request // SetupWithManager sets up the controller with the Manager. func (r *IngressRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - // Uncomment the following line adding a pointer to an instance of the controlled resource as an argument - // For(). + For(&traefikcontainous.IngressRoute{}). + //For(&traefikiov1alpha1.IngressRoute{}). Complete(r) } From da6b9f18129e7764cf75650e57df44c1148280c1 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Fri, 12 Apr 2024 17:08:23 +0200 Subject: [PATCH 05/26] feat(uptime): Read annotations from Traefik - both the old and new IngressRoute kinds - and update mock uptime monitroing provider --- PROJECT | 2 + cmd/main.go | 10 ++- .../controller/ingressroute_controller.go | 84 +++++++++++++++---- internal/model/check.go | 52 ++++++++++++ internal/provider/mock.go | 41 +++++++++ internal/provider/provider.go | 11 +++ 6 files changed, 178 insertions(+), 22 deletions(-) create mode 100644 internal/model/check.go create mode 100644 internal/provider/mock.go create mode 100644 internal/provider/provider.go diff --git a/PROJECT b/PROJECT index e358f97..71dde19 100644 --- a/PROJECT +++ b/PROJECT @@ -9,10 +9,12 @@ projectName: uptime-operator repo: github.com/PDOK/uptime-operator resources: - controller: true + domain: pdok.nl group: traefik.containo.us kind: IngressRoute version: v1alpha1 - controller: true + domain: pdok.nl group: traefik.io kind: IngressRoute version: v1alpha1 diff --git a/cmd/main.go b/cmd/main.go index c882bdf..d7dd899 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -21,6 +21,7 @@ import ( "flag" "os" + "github.com/PDOK/uptime-operator/internal/provider" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -36,7 +37,7 @@ import ( "github.com/PDOK/uptime-operator/internal/controller" traefikcontainous "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1" - traefikiov1alpha1 "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikio/v1alpha1" + traefikio "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikio/v1alpha1" //+kubebuilder:scaffold:imports ) @@ -49,7 +50,7 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(traefikcontainous.AddToScheme(scheme)) - utilruntime.Must(traefikiov1alpha1.AddToScheme(scheme)) + utilruntime.Must(traefikio.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -125,8 +126,9 @@ func main() { } if err = (&controller.IngressRouteReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + UptimeProvider: provider.NewMockUptimeProvider(), // TODO swap with real uptime provider in the future }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "IngressRoute") os.Exit(1) diff --git a/internal/controller/ingressroute_controller.go b/internal/controller/ingressroute_controller.go index 4a7bf2e..6ea27c6 100644 --- a/internal/controller/ingressroute_controller.go +++ b/internal/controller/ingressroute_controller.go @@ -27,49 +27,97 @@ package controller import ( "context" + "github.com/PDOK/uptime-operator/internal/model" + "github.com/PDOK/uptime-operator/internal/provider" + traefikcontainous "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1" + traefikio "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikio/v1alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" - - traefikcontainous "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/predicate" ) -// IngressRouteReconciler reconciles a IngressRoute object +// IngressRouteReconciler reconciles Traefik IngressRoutes with an uptime monitoring (SaaS) provider type IngressRouteReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + UptimeProvider provider.UptimeProvider } -//+kubebuilder:rbac:groups=traefik.containo.us,resources=ingressroutes,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=traefik.containo.us,resources=ingressroutes/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=traefik.containo.us,resources=ingressroutes,verbs=get;list;watch //+kubebuilder:rbac:groups=traefik.containo.us,resources=ingressroutes/finalizers,verbs=update -//+kubebuilder:rbac:groups=traefik.io,resources=ingressroutes,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=traefik.io,resources=ingressroutes/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=traefik.io,resources=ingressroutes,verbs=get;list;watch //+kubebuilder:rbac:groups=traefik.io,resources=ingressroutes/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the IngressRoute object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile func (r *IngressRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := log.FromContext(ctx) - logger.Info("hello world") + annotations, err := r.getAnnotations(ctx, req) + if err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + r.syncWithUptimeProvider(ctx, annotations) + return ctrl.Result{}, nil +} - // TODO(user): your logic here +func (r *IngressRouteReconciler) getAnnotations(ctx context.Context, req ctrl.Request) (map[string]string, error) { + var annotations map[string]string - return ctrl.Result{}, nil + // first reconcile on "traefik.containo.us/v1alpha1" ingress + ingressContainous := &traefikcontainous.IngressRoute{} + if err := r.Get(ctx, req.NamespacedName, ingressContainous); err != nil { + // not found, now reconcile on "traefik.io/v1alpha1" ingress + ingressIo := &traefikio.IngressRoute{} + if err = r.Get(ctx, req.NamespacedName, ingressIo); err != nil { + // still not found, handle error + logger := log.FromContext(ctx) + if apierrors.IsNotFound(err) { + logger.Info("IngressRoute resource not found", "name", req.NamespacedName) + } else { + logger.Error(err, "unable to fetch IngressRoute resource", "error", err) + } + return nil, err + } else { + annotations = ingressIo.Annotations + } + } else { + annotations = ingressContainous.Annotations + } + return annotations, nil +} + +func (r *IngressRouteReconciler) syncWithUptimeProvider(ctx context.Context, annotations map[string]string) { + logger := log.FromContext(ctx) + check := model.NewUptimeCheck(annotations) + if check != nil { + logger.Info("syncing uptime check with id", "id", check.ID) + err := r.UptimeProvider.CreateOrUpdateCheck(*check) + if err != nil { + logger.Error(err, "failed syncing uptime check", "error", err) + } + } } // SetupWithManager sets up the controller with the Manager. func (r *IngressRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { + preCondition := predicate.Or(predicate.GenerationChangedPredicate{}, predicate.AnnotationChangedPredicate{}) + return ctrl.NewControllerManagedBy(mgr). - For(&traefikcontainous.IngressRoute{}). - //For(&traefikiov1alpha1.IngressRoute{}). + Named("uptime-operator"). + Watches( + &traefikcontainous.IngressRoute{}, // watch "traefik.containo.us/v1alpha1" ingresses + &handler.EnqueueRequestForObject{}, + builder.WithPredicates(preCondition)). + Watches( + &traefikio.IngressRoute{}, // watch "traefik.io/v1alpha1" ingresses + &handler.EnqueueRequestForObject{}, + builder.WithPredicates(preCondition)). Complete(r) } diff --git a/internal/model/check.go b/internal/model/check.go new file mode 100644 index 0000000..fb23c22 --- /dev/null +++ b/internal/model/check.go @@ -0,0 +1,52 @@ +package model + +import "strings" + +const ( + annotationBase = "uptime.pdok.nl/" + annotationId = annotationBase + "id" + annotationName = annotationBase + "name" + annotationUrl = annotationBase + "url" + annotationTags = annotationBase + "tags" + annotationRequestHeaders = annotationBase + "request-headers" + annotationStringContains = annotationBase + "response-check-for-string-contains" + annotationStringNotContains = annotationBase + "response-check-for-string-not-contains" +) + +type UptimeCheck struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Tags []string `json:"tags"` + RequestHeaders map[string]string `json:"request_headers"` + StringContains string `json:"string_contains"` + StringNotContains string `json:"string_not_contains"` +} + +func NewUptimeCheck(annotations map[string]string) *UptimeCheck { + id, ok := annotations[annotationId] + if !ok { + return nil + } + return &UptimeCheck{ + ID: id, + Name: annotations[annotationName], + URL: annotations[annotationUrl], + Tags: strings.Split(annotations[annotationTags], ","), + RequestHeaders: kvStringToMap(annotations[annotationRequestHeaders]), + StringContains: annotations[annotationStringContains], + StringNotContains: annotations[annotationStringNotContains], + } +} + +func kvStringToMap(s string) map[string]string { + kvPairs := strings.Split(s, ",") + result := make(map[string]string) + for _, kvPair := range kvPairs { + parts := strings.Split(kvPair, ":") + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + result[key] = value + } + return result +} diff --git a/internal/provider/mock.go b/internal/provider/mock.go new file mode 100644 index 0000000..3a3e1b9 --- /dev/null +++ b/internal/provider/mock.go @@ -0,0 +1,41 @@ +package provider + +import ( + "encoding/json" + "log" + + "github.com/PDOK/uptime-operator/internal/model" +) + +type MockUptimeProvider struct { + checks map[string]model.UptimeCheck +} + +func NewMockUptimeProvider() UptimeProvider { + return &MockUptimeProvider{ + checks: make(map[string]model.UptimeCheck), + } +} + +func (m *MockUptimeProvider) HasCheck(check model.UptimeCheck) bool { + _, ok := m.checks[check.ID] + return ok +} + +func (m *MockUptimeProvider) CreateOrUpdateCheck(check model.UptimeCheck) error { + m.checks[check.ID] = check + + checkJson, _ := json.Marshal(check) + log.Printf("created or updated check %s\n", checkJson) + + return nil +} + +func (m *MockUptimeProvider) DeleteCheck(check model.UptimeCheck) error { + delete(m.checks, check.ID) + + checkJson, _ := json.Marshal(check) + log.Printf("deleted check %s\n", checkJson) + + return nil +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go new file mode 100644 index 0000000..5788562 --- /dev/null +++ b/internal/provider/provider.go @@ -0,0 +1,11 @@ +package provider + +import ( + "github.com/PDOK/uptime-operator/internal/model" +) + +type UptimeProvider interface { + HasCheck(check model.UptimeCheck) bool + CreateOrUpdateCheck(check model.UptimeCheck) error + DeleteCheck(check model.UptimeCheck) error +} From 56c9b73a62775ca2911b3434fc4d216691f9096c Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Fri, 12 Apr 2024 17:08:54 +0200 Subject: [PATCH 06/26] feat(uptime): update manifests --- config/rbac/role.yaml | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 6892aa4..2f4c0c0 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -9,12 +9,8 @@ rules: resources: - ingressroutes verbs: - - create - - delete - get - list - - patch - - update - watch - apiGroups: - traefik.containo.us @@ -22,25 +18,13 @@ rules: - ingressroutes/finalizers verbs: - update -- apiGroups: - - traefik.containo.us - resources: - - ingressroutes/status - verbs: - - get - - patch - - update - apiGroups: - traefik.io resources: - ingressroutes verbs: - - create - - delete - get - list - - patch - - update - watch - apiGroups: - traefik.io @@ -48,11 +32,3 @@ rules: - ingressroutes/finalizers verbs: - update -- apiGroups: - - traefik.io - resources: - - ingressroutes/status - verbs: - - get - - patch - - update From 040310e020324e0a8b030c978fd97fabc13df17c Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Mon, 15 Apr 2024 11:13:58 +0200 Subject: [PATCH 07/26] feat(uptime): add option to watch only specified namespaces for ingress route changes --- cmd/main.go | 26 +++++++++++++++++++++----- internal/util/flag.go | 16 ++++++++++++++++ test/e2e/e2e_suite_test.go | 2 +- test/e2e/e2e_test.go | 4 ++-- 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 internal/util/flag.go diff --git a/cmd/main.go b/cmd/main.go index d7dd899..2a314e9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,6 +22,9 @@ import ( "os" "github.com/PDOK/uptime-operator/internal/provider" + "github.com/PDOK/uptime-operator/internal/util" + "sigs.k8s.io/controller-runtime/pkg/cache" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -60,15 +63,18 @@ func main() { var probeAddr string var secureMetrics bool var enableHTTP2 bool + var namespaces util.SliceFlag flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.BoolVar(&secureMetrics, "metrics-secure", false, - "If set the metrics endpoint is served securely") + "If set the metrics endpoint is served securely.") flag.BoolVar(&enableHTTP2, "enable-http2", false, - "If set, HTTP/2 will be enabled for the metrics and webhook servers") + "If set, HTTP/2 will be enabled for the metrics and webhook servers.") + flag.Var(&namespaces, "namespace", "Namespace(s) to watch for changes."+ + "Specify this flag multiple times for each namespace to watch. When not provided all namespaces will be watched.") opts := zap.Options{ Development: true, } @@ -88,7 +94,7 @@ func main() { c.NextProtos = []string{"http/1.1"} } - tlsOpts := []func(*tls.Config){} + var tlsOpts []func(*tls.Config) if !enableHTTP2 { tlsOpts = append(tlsOpts, disableHTTP2) } @@ -97,7 +103,7 @@ func main() { TLSOpts: tlsOpts, }) - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + managerOpts := ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{ BindAddress: metricsAddr, @@ -119,7 +125,17 @@ func main() { // if you are doing or is intended to do any operation such as perform cleanups // after the manager stops then its usage might be unsafe. // LeaderElectionReleaseOnCancel: true, - }) + } + + if len(namespaces) > 0 { + namespacesToWatch := make(map[string]cache.Config) + for _, namespace := range namespaces { + namespacesToWatch[namespace] = cache.Config{} + } + managerOpts.Cache.DefaultNamespaces = namespacesToWatch + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), managerOpts) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) diff --git a/internal/util/flag.go b/internal/util/flag.go new file mode 100644 index 0000000..1701ea6 --- /dev/null +++ b/internal/util/flag.go @@ -0,0 +1,16 @@ +package util + +import ( + "strings" +) + +type SliceFlag []string + +func (sf *SliceFlag) String() string { + return strings.Join(*sf, ",") +} + +func (sf *SliceFlag) Set(value string) error { + *sf = append(*sf, value) + return nil +} diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 041a6c9..a6e3b1a 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -35,6 +35,6 @@ import ( // Run e2e tests using the Ginkgo runner. func TestE2E(t *testing.T) { RegisterFailHandler(Fail) - fmt.Fprintf(GinkgoWriter, "Starting ogcapi-operator suite\n") + fmt.Fprintf(GinkgoWriter, "Starting uptime-operator suite\n") RunSpecs(t, "e2e suite") } diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 7d1079d..13c45b8 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -35,7 +35,7 @@ import ( "github.com/PDOK/uptime-operator/test/utils" ) -const namespace = "ogcapi-operator-system" +const namespace = "uptime-operator-system" var _ = Describe("controller", Ordered, func() { BeforeAll(func() { @@ -68,7 +68,7 @@ var _ = Describe("controller", Ordered, func() { var err error // projectimage stores the name of the image used in the example - var projectimage = "example.com/ogcapi-operator:v0.0.1" + var projectimage = "example.com/uptime-operator:v0.0.1" By("building the manager(Operator) image") cmd := exec.Command("make", "docker-build", "IMG="+projectimage) From 92984b03a0e6b584b984cb2551c38859feb62d18 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Mon, 15 Apr 2024 11:22:39 +0200 Subject: [PATCH 08/26] chore(lint): fix linting --- cmd/main.go | 1 + internal/controller/ingressroute_controller.go | 9 ++------- internal/controller/ingressroute_controller_test.go | 2 +- internal/controller/suite_test.go | 4 ++-- internal/model/check.go | 8 ++++---- internal/provider/mock.go | 8 ++++---- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 2a314e9..294a221 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -57,6 +57,7 @@ func init() { //+kubebuilder:scaffold:scheme } +//nolint:funlen func main() { var metricsAddr string var enableLeaderElection bool diff --git a/internal/controller/ingressroute_controller.go b/internal/controller/ingressroute_controller.go index 6ea27c6..bab8beb 100644 --- a/internal/controller/ingressroute_controller.go +++ b/internal/controller/ingressroute_controller.go @@ -68,8 +68,6 @@ func (r *IngressRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request } func (r *IngressRouteReconciler) getAnnotations(ctx context.Context, req ctrl.Request) (map[string]string, error) { - var annotations map[string]string - // first reconcile on "traefik.containo.us/v1alpha1" ingress ingressContainous := &traefikcontainous.IngressRoute{} if err := r.Get(ctx, req.NamespacedName, ingressContainous); err != nil { @@ -84,13 +82,10 @@ func (r *IngressRouteReconciler) getAnnotations(ctx context.Context, req ctrl.Re logger.Error(err, "unable to fetch IngressRoute resource", "error", err) } return nil, err - } else { - annotations = ingressIo.Annotations } - } else { - annotations = ingressContainous.Annotations + return ingressIo.Annotations, nil } - return annotations, nil + return ingressContainous.Annotations, nil } func (r *IngressRouteReconciler) syncWithUptimeProvider(ctx context.Context, annotations map[string]string) { diff --git a/internal/controller/ingressroute_controller_test.go b/internal/controller/ingressroute_controller_test.go index c8e64fe..a15df12 100644 --- a/internal/controller/ingressroute_controller_test.go +++ b/internal/controller/ingressroute_controller_test.go @@ -25,7 +25,7 @@ SOFTWARE. package controller import ( - . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd ) var _ = Describe("IngressRoute Controller", func() { diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 5f11c74..05f5da4 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -30,8 +30,8 @@ import ( "runtime" "testing" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" + . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd + . "github.com/onsi/gomega" //nolint:revive // ginkgo bdd "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" diff --git a/internal/model/check.go b/internal/model/check.go index fb23c22..50df9a6 100644 --- a/internal/model/check.go +++ b/internal/model/check.go @@ -4,9 +4,9 @@ import "strings" const ( annotationBase = "uptime.pdok.nl/" - annotationId = annotationBase + "id" + annotationID = annotationBase + "id" annotationName = annotationBase + "name" - annotationUrl = annotationBase + "url" + annotationUURL = annotationBase + "url" annotationTags = annotationBase + "tags" annotationRequestHeaders = annotationBase + "request-headers" annotationStringContains = annotationBase + "response-check-for-string-contains" @@ -24,14 +24,14 @@ type UptimeCheck struct { } func NewUptimeCheck(annotations map[string]string) *UptimeCheck { - id, ok := annotations[annotationId] + id, ok := annotations[annotationID] if !ok { return nil } return &UptimeCheck{ ID: id, Name: annotations[annotationName], - URL: annotations[annotationUrl], + URL: annotations[annotationUURL], Tags: strings.Split(annotations[annotationTags], ","), RequestHeaders: kvStringToMap(annotations[annotationRequestHeaders]), StringContains: annotations[annotationStringContains], diff --git a/internal/provider/mock.go b/internal/provider/mock.go index 3a3e1b9..8d9628b 100644 --- a/internal/provider/mock.go +++ b/internal/provider/mock.go @@ -25,8 +25,8 @@ func (m *MockUptimeProvider) HasCheck(check model.UptimeCheck) bool { func (m *MockUptimeProvider) CreateOrUpdateCheck(check model.UptimeCheck) error { m.checks[check.ID] = check - checkJson, _ := json.Marshal(check) - log.Printf("created or updated check %s\n", checkJson) + checkJSON, _ := json.Marshal(check) + log.Printf("created or updated check %s\n", checkJSON) return nil } @@ -34,8 +34,8 @@ func (m *MockUptimeProvider) CreateOrUpdateCheck(check model.UptimeCheck) error func (m *MockUptimeProvider) DeleteCheck(check model.UptimeCheck) error { delete(m.checks, check.ID) - checkJson, _ := json.Marshal(check) - log.Printf("deleted check %s\n", checkJson) + checkJSON, _ := json.Marshal(check) + log.Printf("deleted check %s\n", checkJSON) return nil } From 55e0712d3f1f6bcc070234ba1d83250fe7440b6e Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Mon, 15 Apr 2024 12:33:27 +0200 Subject: [PATCH 09/26] feat(uptime): add tag to indicate to humans that a given check is managed by the operator. --- cmd/main.go | 2 +- internal/model/check.go | 29 ++++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 294a221..753e4ef 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -74,7 +74,7 @@ func main() { "If set the metrics endpoint is served securely.") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers.") - flag.Var(&namespaces, "namespace", "Namespace(s) to watch for changes."+ + flag.Var(&namespaces, "namespace", "Namespace(s) to watch for changes. "+ "Specify this flag multiple times for each namespace to watch. When not provided all namespaces will be watched.") opts := zap.Options{ Development: true, diff --git a/internal/model/check.go b/internal/model/check.go index 50df9a6..7885736 100644 --- a/internal/model/check.go +++ b/internal/model/check.go @@ -1,16 +1,22 @@ package model -import "strings" +import ( + "slices" + "strings" +) const ( annotationBase = "uptime.pdok.nl/" annotationID = annotationBase + "id" annotationName = annotationBase + "name" - annotationUURL = annotationBase + "url" + annotationURL = annotationBase + "url" annotationTags = annotationBase + "tags" annotationRequestHeaders = annotationBase + "request-headers" annotationStringContains = annotationBase + "response-check-for-string-contains" annotationStringNotContains = annotationBase + "response-check-for-string-not-contains" + + // Indicate to humans that the given check is managed by the operator. + tagManagedBy = "managed-by-uptime-operator" ) type UptimeCheck struct { @@ -28,15 +34,19 @@ func NewUptimeCheck(annotations map[string]string) *UptimeCheck { if !ok { return nil } - return &UptimeCheck{ + check := &UptimeCheck{ ID: id, Name: annotations[annotationName], - URL: annotations[annotationUURL], - Tags: strings.Split(annotations[annotationTags], ","), + URL: annotations[annotationURL], + Tags: stringToSlice(annotations[annotationTags]), RequestHeaders: kvStringToMap(annotations[annotationRequestHeaders]), StringContains: annotations[annotationStringContains], StringNotContains: annotations[annotationStringNotContains], } + if !slices.Contains(check.Tags, tagManagedBy) { + check.Tags = append(check.Tags, tagManagedBy) + } + return check } func kvStringToMap(s string) map[string]string { @@ -50,3 +60,12 @@ func kvStringToMap(s string) map[string]string { } return result } + +func stringToSlice(s string) []string { + var result []string + splits := strings.Split(s, ",") + for _, part := range splits { + result = append(result, strings.TrimSpace(part)) + } + return result +} From 65b98a5603e0a28543e37570f0d4b7673d0ee415 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Mon, 15 Apr 2024 13:42:31 +0200 Subject: [PATCH 10/26] feat(deletion): implement deletion of checks --- .../controller/ingressroute_controller.go | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/internal/controller/ingressroute_controller.go b/internal/controller/ingressroute_controller.go index bab8beb..867cf17 100644 --- a/internal/controller/ingressroute_controller.go +++ b/internal/controller/ingressroute_controller.go @@ -36,6 +36,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -59,6 +60,10 @@ type IngressRouteReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile func (r *IngressRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + err := r.checkForDeletion(ctx, req) + if err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } annotations, err := r.getAnnotations(ctx, req) if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) @@ -88,6 +93,60 @@ func (r *IngressRouteReconciler) getAnnotations(ctx context.Context, req ctrl.Re return ingressContainous.Annotations, nil } +func (r *IngressRouteReconciler) checkForDeletion(ctx context.Context, req ctrl.Request) error { + // first reconcile on "traefik.containo.us/v1alpha1" ingress + ingressContainous := &traefikcontainous.IngressRoute{} + if err := r.Get(ctx, req.NamespacedName, ingressContainous); err != nil { + // not found, now reconcile on "traefik.io/v1alpha1" ingress + ingressIo := &traefikio.IngressRoute{} + if err = r.Get(ctx, req.NamespacedName, ingressIo); err != nil { + return nil + } + finalizerName := "uptime-operator." + ingressIo.Name + "/finalizer" + shouldContinue, err := finalizeIfNecessary(ctx, r.Client, ingressIo, finalizerName, func() error { + r.deleteWithUptimeProvider(ctx, ingressIo.Annotations) + return nil + }) + if !shouldContinue || err != nil { + return err + } + } + finalizerName := "uptime-operator." + ingressContainous.Name + "/finalizer" + shouldContinue, err := finalizeIfNecessary(ctx, r.Client, ingressContainous, finalizerName, func() error { + r.deleteWithUptimeProvider(ctx, ingressContainous.Annotations) + return nil + }) + if !shouldContinue || err != nil { + return err + } + return nil +} + +func finalizeIfNecessary(ctx context.Context, c client.Client, obj client.Object, finalizerName string, finalizer func() error) (shouldContinue bool, err error) { + // not under deletion, ensure finalizer annotation + if obj.GetDeletionTimestamp().IsZero() { + if !controllerutil.ContainsFinalizer(obj, finalizerName) { + controllerutil.AddFinalizer(obj, finalizerName) + err = c.Update(ctx, obj) + return false, err + } + return true, nil + } + + // under deletion but not our finalizer annotation, do nothing + if !controllerutil.ContainsFinalizer(obj, finalizerName) { + return false, nil + } + + // run finalizer and remove annotation + if err = finalizer(); err != nil { + return false, err + } + controllerutil.RemoveFinalizer(obj, finalizerName) + err = c.Update(ctx, obj) + return false, err +} + func (r *IngressRouteReconciler) syncWithUptimeProvider(ctx context.Context, annotations map[string]string) { logger := log.FromContext(ctx) check := model.NewUptimeCheck(annotations) @@ -100,6 +159,18 @@ func (r *IngressRouteReconciler) syncWithUptimeProvider(ctx context.Context, ann } } +func (r *IngressRouteReconciler) deleteWithUptimeProvider(ctx context.Context, annotations map[string]string) { + logger := log.FromContext(ctx) + check := model.NewUptimeCheck(annotations) + if check != nil { + logger.Info("deleting uptime check with id", "id", check.ID) + err := r.UptimeProvider.DeleteCheck(*check) + if err != nil { + logger.Error(err, "failed deleting uptime check", "error", err) + } + } +} + // SetupWithManager sets up the controller with the Manager. func (r *IngressRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { preCondition := predicate.Or(predicate.GenerationChangedPredicate{}, predicate.AnnotationChangedPredicate{}) From a020f750064eac5ad6f158162b50fd33367e27b0 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 16 Apr 2024 10:09:38 +0200 Subject: [PATCH 11/26] feat(deletion): refactor deletion of checks --- .../controller/ingressroute_controller.go | 56 ++++++------------- internal/model/check.go | 16 +++--- 2 files changed, 25 insertions(+), 47 deletions(-) diff --git a/internal/controller/ingressroute_controller.go b/internal/controller/ingressroute_controller.go index 867cf17..a6bddf8 100644 --- a/internal/controller/ingressroute_controller.go +++ b/internal/controller/ingressroute_controller.go @@ -42,6 +42,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" ) +const operatorName = "uptime-operator" + +var finalizerName = model.AnnotationBase + "/finalizer" + // IngressRouteReconciler reconciles Traefik IngressRoutes with an uptime monitoring (SaaS) provider type IngressRouteReconciler struct { client.Client @@ -60,23 +64,26 @@ type IngressRouteReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.17.0/pkg/reconcile func (r *IngressRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - err := r.checkForDeletion(ctx, req) + ingressRoute, err := r.getIngressRoute(ctx, req) if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - annotations, err := r.getAnnotations(ctx, req) - if err != nil { + shouldContinue, err := finalizeIfNecessary(ctx, r.Client, ingressRoute, finalizerName, func() error { + r.deleteWithUptimeProvider(ctx, ingressRoute.GetAnnotations()) + return nil + }) + if !shouldContinue || err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - r.syncWithUptimeProvider(ctx, annotations) + r.syncWithUptimeProvider(ctx, ingressRoute.GetAnnotations()) return ctrl.Result{}, nil } -func (r *IngressRouteReconciler) getAnnotations(ctx context.Context, req ctrl.Request) (map[string]string, error) { - // first reconcile on "traefik.containo.us/v1alpha1" ingress +func (r *IngressRouteReconciler) getIngressRoute(ctx context.Context, req ctrl.Request) (client.Object, error) { + // first try getting "traefik.containo.us/v1alpha1" ingress ingressContainous := &traefikcontainous.IngressRoute{} if err := r.Get(ctx, req.NamespacedName, ingressContainous); err != nil { - // not found, now reconcile on "traefik.io/v1alpha1" ingress + // not found, now try getting "traefik.io/v1alpha1" ingress ingressIo := &traefikio.IngressRoute{} if err = r.Get(ctx, req.NamespacedName, ingressIo); err != nil { // still not found, handle error @@ -88,38 +95,9 @@ func (r *IngressRouteReconciler) getAnnotations(ctx context.Context, req ctrl.Re } return nil, err } - return ingressIo.Annotations, nil - } - return ingressContainous.Annotations, nil -} - -func (r *IngressRouteReconciler) checkForDeletion(ctx context.Context, req ctrl.Request) error { - // first reconcile on "traefik.containo.us/v1alpha1" ingress - ingressContainous := &traefikcontainous.IngressRoute{} - if err := r.Get(ctx, req.NamespacedName, ingressContainous); err != nil { - // not found, now reconcile on "traefik.io/v1alpha1" ingress - ingressIo := &traefikio.IngressRoute{} - if err = r.Get(ctx, req.NamespacedName, ingressIo); err != nil { - return nil - } - finalizerName := "uptime-operator." + ingressIo.Name + "/finalizer" - shouldContinue, err := finalizeIfNecessary(ctx, r.Client, ingressIo, finalizerName, func() error { - r.deleteWithUptimeProvider(ctx, ingressIo.Annotations) - return nil - }) - if !shouldContinue || err != nil { - return err - } - } - finalizerName := "uptime-operator." + ingressContainous.Name + "/finalizer" - shouldContinue, err := finalizeIfNecessary(ctx, r.Client, ingressContainous, finalizerName, func() error { - r.deleteWithUptimeProvider(ctx, ingressContainous.Annotations) - return nil - }) - if !shouldContinue || err != nil { - return err + return ingressIo, nil } - return nil + return ingressContainous, nil } func finalizeIfNecessary(ctx context.Context, c client.Client, obj client.Object, finalizerName string, finalizer func() error) (shouldContinue bool, err error) { @@ -176,7 +154,7 @@ func (r *IngressRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { preCondition := predicate.Or(predicate.GenerationChangedPredicate{}, predicate.AnnotationChangedPredicate{}) return ctrl.NewControllerManagedBy(mgr). - Named("uptime-operator"). + Named(operatorName). Watches( &traefikcontainous.IngressRoute{}, // watch "traefik.containo.us/v1alpha1" ingresses &handler.EnqueueRequestForObject{}, diff --git a/internal/model/check.go b/internal/model/check.go index 7885736..26771b8 100644 --- a/internal/model/check.go +++ b/internal/model/check.go @@ -6,14 +6,14 @@ import ( ) const ( - annotationBase = "uptime.pdok.nl/" - annotationID = annotationBase + "id" - annotationName = annotationBase + "name" - annotationURL = annotationBase + "url" - annotationTags = annotationBase + "tags" - annotationRequestHeaders = annotationBase + "request-headers" - annotationStringContains = annotationBase + "response-check-for-string-contains" - annotationStringNotContains = annotationBase + "response-check-for-string-not-contains" + AnnotationBase = "uptime.pdok.nl" + annotationID = AnnotationBase + "/id" + annotationName = AnnotationBase + "/name" + annotationURL = AnnotationBase + "/url" + annotationTags = AnnotationBase + "/tags" + annotationRequestHeaders = AnnotationBase + "/request-headers" + annotationStringContains = AnnotationBase + "/response-check-for-string-contains" + annotationStringNotContains = AnnotationBase + "/response-check-for-string-not-contains" // Indicate to humans that the given check is managed by the operator. tagManagedBy = "managed-by-uptime-operator" From 09800035bf5579071105ff6458917ce9a3809bb9 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 16 Apr 2024 17:08:41 +0200 Subject: [PATCH 12/26] feat(slack): add slack notifications --- cmd/main.go | 12 ++-- go.mod | 10 +-- go.sum | 25 ++++--- .../controller/ingressroute_controller.go | 44 +++--------- internal/model/check.go | 9 ++- internal/model/mutation.go | 8 +++ internal/{provider => service}/mock.go | 6 +- internal/{provider => service}/provider.go | 2 +- internal/service/service.go | 69 +++++++++++++++++++ internal/util/slack.go | 33 +++++++++ 10 files changed, 159 insertions(+), 59 deletions(-) create mode 100644 internal/model/mutation.go rename internal/{provider => service}/mock.go (84%) rename internal/{provider => service}/provider.go (93%) create mode 100644 internal/service/service.go create mode 100644 internal/util/slack.go diff --git a/cmd/main.go b/cmd/main.go index 753e4ef..109c902 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -21,7 +21,7 @@ import ( "flag" "os" - "github.com/PDOK/uptime-operator/internal/provider" + "github.com/PDOK/uptime-operator/internal/service" "github.com/PDOK/uptime-operator/internal/util" "sigs.k8s.io/controller-runtime/pkg/cache" @@ -65,6 +65,8 @@ func main() { var secureMetrics bool var enableHTTP2 bool var namespaces util.SliceFlag + var slackChannel string + var slackToken string flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, @@ -76,6 +78,8 @@ func main() { "If set, HTTP/2 will be enabled for the metrics and webhook servers.") flag.Var(&namespaces, "namespace", "Namespace(s) to watch for changes. "+ "Specify this flag multiple times for each namespace to watch. When not provided all namespaces will be watched.") + flag.StringVar(&slackChannel, "slack-channel", "", "The Slack Channel ID for posting updates when uptime checks are mutated.") + flag.StringVar(&slackToken, "slack-token", "", "The token required to access the given Slack channel.") opts := zap.Options{ Development: true, } @@ -143,9 +147,9 @@ func main() { } if err = (&controller.IngressRouteReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - UptimeProvider: provider.NewMockUptimeProvider(), // TODO swap with real uptime provider in the future + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + UptimeCheckService: service.New("mock", slackToken, slackChannel), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "IngressRoute") os.Exit(1) diff --git a/go.mod b/go.mod index 6436259..1af3cc3 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22 require ( github.com/onsi/ginkgo/v2 v2.17.1 github.com/onsi/gomega v1.32.0 + github.com/slack-go/slack v0.12.5 github.com/traefik/traefik/v2 v2.11.0 k8s.io/apimachinery v0.29.3 k8s.io/client-go v0.29.3 @@ -38,6 +39,7 @@ require ( github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect github.com/google/uuid v1.4.0 // indirect github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -58,13 +60,13 @@ require ( github.com/traefik/paerser v0.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.20.0 // indirect + golang.org/x/net v0.24.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.17.0 // indirect diff --git a/go.sum b/go.sum index 48a01bf..72e5f7a 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -46,6 +48,7 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -58,6 +61,9 @@ github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -109,11 +115,14 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slack-go/slack v0.12.5 h1:ddZ6uz6XVaB+3MTDhoW04gG+Vc/M/X1ctC+wssy2cqs= +github.com/slack-go/slack v0.12.5/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -138,8 +147,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -151,8 +160,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -164,10 +173,10 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/internal/controller/ingressroute_controller.go b/internal/controller/ingressroute_controller.go index a6bddf8..b55cd3b 100644 --- a/internal/controller/ingressroute_controller.go +++ b/internal/controller/ingressroute_controller.go @@ -27,8 +27,8 @@ package controller import ( "context" - "github.com/PDOK/uptime-operator/internal/model" - "github.com/PDOK/uptime-operator/internal/provider" + m "github.com/PDOK/uptime-operator/internal/model" + "github.com/PDOK/uptime-operator/internal/service" traefikcontainous "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1" traefikio "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikio/v1alpha1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -42,15 +42,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" ) -const operatorName = "uptime-operator" - -var finalizerName = model.AnnotationBase + "/finalizer" - // IngressRouteReconciler reconciles Traefik IngressRoutes with an uptime monitoring (SaaS) provider type IngressRouteReconciler struct { client.Client - Scheme *runtime.Scheme - UptimeProvider provider.UptimeProvider + Scheme *runtime.Scheme + UptimeCheckService *service.UptimeCheckService } //+kubebuilder:rbac:groups=traefik.containo.us,resources=ingressroutes,verbs=get;list;watch @@ -68,14 +64,14 @@ func (r *IngressRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - shouldContinue, err := finalizeIfNecessary(ctx, r.Client, ingressRoute, finalizerName, func() error { - r.deleteWithUptimeProvider(ctx, ingressRoute.GetAnnotations()) + shouldContinue, err := finalizeIfNecessary(ctx, r.Client, ingressRoute, m.AnnotationFinalizer, func() error { + r.UptimeCheckService.Mutate(ctx, m.Delete, ingressRoute.GetAnnotations()) return nil }) if !shouldContinue || err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - r.syncWithUptimeProvider(ctx, ingressRoute.GetAnnotations()) + r.UptimeCheckService.Mutate(ctx, m.CreateOrUpdate, ingressRoute.GetAnnotations()) return ctrl.Result{}, nil } @@ -125,36 +121,12 @@ func finalizeIfNecessary(ctx context.Context, c client.Client, obj client.Object return false, err } -func (r *IngressRouteReconciler) syncWithUptimeProvider(ctx context.Context, annotations map[string]string) { - logger := log.FromContext(ctx) - check := model.NewUptimeCheck(annotations) - if check != nil { - logger.Info("syncing uptime check with id", "id", check.ID) - err := r.UptimeProvider.CreateOrUpdateCheck(*check) - if err != nil { - logger.Error(err, "failed syncing uptime check", "error", err) - } - } -} - -func (r *IngressRouteReconciler) deleteWithUptimeProvider(ctx context.Context, annotations map[string]string) { - logger := log.FromContext(ctx) - check := model.NewUptimeCheck(annotations) - if check != nil { - logger.Info("deleting uptime check with id", "id", check.ID) - err := r.UptimeProvider.DeleteCheck(*check) - if err != nil { - logger.Error(err, "failed deleting uptime check", "error", err) - } - } -} - // SetupWithManager sets up the controller with the Manager. func (r *IngressRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { preCondition := predicate.Or(predicate.GenerationChangedPredicate{}, predicate.AnnotationChangedPredicate{}) return ctrl.NewControllerManagedBy(mgr). - Named(operatorName). + Named(m.OperatorName). Watches( &traefikcontainous.IngressRoute{}, // watch "traefik.containo.us/v1alpha1" ingresses &handler.EnqueueRequestForObject{}, diff --git a/internal/model/check.go b/internal/model/check.go index 26771b8..e4475d5 100644 --- a/internal/model/check.go +++ b/internal/model/check.go @@ -6,7 +6,13 @@ import ( ) const ( + OperatorName = "uptime-operator" + + // Indicate to humans that the given check is managed by the operator. + tagManagedBy = "managed-by-" + OperatorName + AnnotationBase = "uptime.pdok.nl" + AnnotationFinalizer = AnnotationBase + "/finalizer" annotationID = AnnotationBase + "/id" annotationName = AnnotationBase + "/name" annotationURL = AnnotationBase + "/url" @@ -14,9 +20,6 @@ const ( annotationRequestHeaders = AnnotationBase + "/request-headers" annotationStringContains = AnnotationBase + "/response-check-for-string-contains" annotationStringNotContains = AnnotationBase + "/response-check-for-string-not-contains" - - // Indicate to humans that the given check is managed by the operator. - tagManagedBy = "managed-by-uptime-operator" ) type UptimeCheck struct { diff --git a/internal/model/mutation.go b/internal/model/mutation.go new file mode 100644 index 0000000..9158090 --- /dev/null +++ b/internal/model/mutation.go @@ -0,0 +1,8 @@ +package model + +type Mutation string + +const ( + CreateOrUpdate Mutation = "create-or-update" + Delete Mutation = "delete" +) diff --git a/internal/provider/mock.go b/internal/service/mock.go similarity index 84% rename from internal/provider/mock.go rename to internal/service/mock.go index 8d9628b..8c1aa0b 100644 --- a/internal/provider/mock.go +++ b/internal/service/mock.go @@ -1,4 +1,4 @@ -package provider +package service import ( "encoding/json" @@ -26,7 +26,7 @@ func (m *MockUptimeProvider) CreateOrUpdateCheck(check model.UptimeCheck) error m.checks[check.ID] = check checkJSON, _ := json.Marshal(check) - log.Printf("created or updated check %s\n", checkJSON) + log.Printf("MOCK: created or updated check %s\n", checkJSON) return nil } @@ -35,7 +35,7 @@ func (m *MockUptimeProvider) DeleteCheck(check model.UptimeCheck) error { delete(m.checks, check.ID) checkJSON, _ := json.Marshal(check) - log.Printf("deleted check %s\n", checkJSON) + log.Printf("MOCK: deleted check %s\n", checkJSON) return nil } diff --git a/internal/provider/provider.go b/internal/service/provider.go similarity index 93% rename from internal/provider/provider.go rename to internal/service/provider.go index 5788562..fde8047 100644 --- a/internal/provider/provider.go +++ b/internal/service/provider.go @@ -1,4 +1,4 @@ -package provider +package service import ( "github.com/PDOK/uptime-operator/internal/model" diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 0000000..6eacdde --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,69 @@ +package service + +import ( + "context" + "fmt" + + m "github.com/PDOK/uptime-operator/internal/model" + "github.com/PDOK/uptime-operator/internal/util" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type UptimeCheckService struct { + provider UptimeProvider + slack *util.Slack +} + +func New(provider string, slackToken string, slackChannel string) *UptimeCheckService { + var p UptimeProvider + switch provider { + case "mock": + p = NewMockUptimeProvider() + // TODO add new case(s) for actual uptime monitoring SaaS providers + } + + var slack *util.Slack + if slackToken != "" && slackChannel != "" { + slack = util.NewSlack(slackToken, slackChannel) + } + + return &UptimeCheckService{ + slack: slack, + provider: p, + } +} + +func (r *UptimeCheckService) Mutate(ctx context.Context, mutation m.Mutation, annotations map[string]string) { + check := m.NewUptimeCheck(annotations) + if check != nil { + switch mutation { + case m.CreateOrUpdate: + err := r.provider.CreateOrUpdateCheck(*check) + r.logMutation(ctx, err, mutation, check) + case m.Delete: + err := r.provider.DeleteCheck(*check) + r.logMutation(ctx, err, mutation, check) + } + } +} + +func (r *UptimeCheckService) logMutation(ctx context.Context, err error, mutation m.Mutation, check *m.UptimeCheck) { + if err != nil { + msg := fmt.Sprintf("%s of uptime check '%s' (id: %s) failed", string(mutation), check.Name, check.ID) + log.FromContext(ctx).Error(err, msg, "check", check) + if r.slack != nil { + r.slack.SendSlackMessage(ctx, ":large_red_square: "+msg) + } + + } else { + msg := fmt.Sprintf("%s of uptime check '%s' (id: %s) succeeded", string(mutation), check.Name, check.ID) + log.FromContext(ctx).Info(msg) + if r.slack != nil { + emoji := ":large_green_square: " + if mutation == m.Delete { + emoji = ":warning: " + } + r.slack.SendSlackMessage(ctx, emoji+msg) + } + } +} diff --git a/internal/util/slack.go b/internal/util/slack.go new file mode 100644 index 0000000..becef0e --- /dev/null +++ b/internal/util/slack.go @@ -0,0 +1,33 @@ +package util + +import ( + "context" + + "github.com/PDOK/uptime-operator/internal/model" + "github.com/slack-go/slack" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type Slack struct { + client *slack.Client + channelID string +} + +func NewSlack(token, channelID string) *Slack { + return &Slack{ + client: slack.New(token), + channelID: channelID, + } +} + +func (s *Slack) SendSlackMessage(ctx context.Context, message string) { + logger := log.FromContext(ctx) + channelID, timestamp, err := s.client.PostMessageContext(ctx, s.channelID, + slack.MsgOptionText(message, false), + slack.MsgOptionUsername(model.OperatorName), + slack.MsgOptionIconEmoji(":robot_face:"), + ) + if err != nil { + logger.Error(err, "failed to post Slack message", "message", message, "channel", channelID, "timestamp", timestamp) + } +} From 1981182a06e5502b3bda246a390db6f90b9983d7 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 16 Apr 2024 17:12:25 +0200 Subject: [PATCH 13/26] feat(slack): add slack notifications --- internal/service/service.go | 7 +++---- internal/{util => service}/slack.go | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) rename internal/{util => service}/slack.go (98%) diff --git a/internal/service/service.go b/internal/service/service.go index 6eacdde..382304e 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -5,13 +5,12 @@ import ( "fmt" m "github.com/PDOK/uptime-operator/internal/model" - "github.com/PDOK/uptime-operator/internal/util" "sigs.k8s.io/controller-runtime/pkg/log" ) type UptimeCheckService struct { provider UptimeProvider - slack *util.Slack + slack *Slack } func New(provider string, slackToken string, slackChannel string) *UptimeCheckService { @@ -22,9 +21,9 @@ func New(provider string, slackToken string, slackChannel string) *UptimeCheckSe // TODO add new case(s) for actual uptime monitoring SaaS providers } - var slack *util.Slack + var slack *Slack if slackToken != "" && slackChannel != "" { - slack = util.NewSlack(slackToken, slackChannel) + slack = NewSlack(slackToken, slackChannel) } return &UptimeCheckService{ diff --git a/internal/util/slack.go b/internal/service/slack.go similarity index 98% rename from internal/util/slack.go rename to internal/service/slack.go index becef0e..7c2e139 100644 --- a/internal/util/slack.go +++ b/internal/service/slack.go @@ -1,4 +1,4 @@ -package util +package service import ( "context" From b0b0ec0f4ee42463623d67ddb1696f593235b1a2 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 16 Apr 2024 17:13:08 +0200 Subject: [PATCH 14/26] feat(slack): polishing --- internal/service/{ => providers}/mock.go | 5 +++-- internal/service/service.go | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) rename internal/service/{ => providers}/mock.go (86%) diff --git a/internal/service/mock.go b/internal/service/providers/mock.go similarity index 86% rename from internal/service/mock.go rename to internal/service/providers/mock.go index 8c1aa0b..062ced8 100644 --- a/internal/service/mock.go +++ b/internal/service/providers/mock.go @@ -1,17 +1,18 @@ -package service +package providers import ( "encoding/json" "log" "github.com/PDOK/uptime-operator/internal/model" + "github.com/PDOK/uptime-operator/internal/service" ) type MockUptimeProvider struct { checks map[string]model.UptimeCheck } -func NewMockUptimeProvider() UptimeProvider { +func NewMockUptimeProvider() service.UptimeProvider { return &MockUptimeProvider{ checks: make(map[string]model.UptimeCheck), } diff --git a/internal/service/service.go b/internal/service/service.go index 382304e..a39f63f 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -5,6 +5,7 @@ import ( "fmt" m "github.com/PDOK/uptime-operator/internal/model" + "github.com/PDOK/uptime-operator/internal/service/providers" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -17,7 +18,7 @@ func New(provider string, slackToken string, slackChannel string) *UptimeCheckSe var p UptimeProvider switch provider { case "mock": - p = NewMockUptimeProvider() + p = providers.NewMockUptimeProvider() // TODO add new case(s) for actual uptime monitoring SaaS providers } From d9bb56b77f3c0848f01fc88fe21ff256112ac21b Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 16 Apr 2024 20:35:34 +0200 Subject: [PATCH 15/26] chore(build): fix build --- internal/service/providers/mock.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/service/providers/mock.go b/internal/service/providers/mock.go index 062ced8..37b1d5a 100644 --- a/internal/service/providers/mock.go +++ b/internal/service/providers/mock.go @@ -5,14 +5,13 @@ import ( "log" "github.com/PDOK/uptime-operator/internal/model" - "github.com/PDOK/uptime-operator/internal/service" ) type MockUptimeProvider struct { checks map[string]model.UptimeCheck } -func NewMockUptimeProvider() service.UptimeProvider { +func NewMockUptimeProvider() *MockUptimeProvider { return &MockUptimeProvider{ checks: make(map[string]model.UptimeCheck), } From 45faf44ad0ff67148bff2eaefa8ea0078e5f3320 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 16 Apr 2024 21:20:39 +0200 Subject: [PATCH 16/26] chore(build): fix lint + readme --- README.md | 148 +++++++++++++++--------------------- cmd/main.go | 4 +- internal/service/service.go | 27 ++++--- 3 files changed, 77 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 8e5cce7..214fee9 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,78 @@ # uptime-operator -// TODO(user): Add simple overview of use/purpose -## Description -// TODO(user): An in-depth paragraph about your project and overview of use - -## Getting Started - -### Prerequisites -- go version v1.21.0+ -- docker version 17.03+. -- kubectl version v1.11.3+. -- Access to a Kubernetes v1.11.3+ cluster. - -### To Deploy on the cluster -**Build and push your image to the location specified by `IMG`:** - -```sh -make docker-build docker-push IMG=/uptime-operator:tag -``` - -**NOTE:** This image ought to be published in the personal registry you specified. -And it is required to have access to pull the image from the working environment. -Make sure you have the proper permission to the registry if the above commands don’t work. - -**Install the CRDs into the cluster:** - -```sh -make install +Kubernetes Operator to watch Traefik IngressRoute(s) and register these with a (SaaS) uptime monitoring provider to watch availability. + +## Annotations + +Traefik `IngressRoute` resource should be annotated in order to successfully register an uptime check. For example: + +```yaml +apiVersion: traefik.containo.us/v1alpha1 +kind: IngressRoute +metadata: + name: my-sweet-route + annotations: + uptime.pdok.nl/id: "Random string to uniquely identify this check with the provider" + uptime.pdok.nl/name: "Logical name of the check" + uptime.pdok.nl/url: "https://site.example/service/wms/v1_0" + uptime.pdok.nl/tags: "metadata,separated,by,commas" + uptime.pdok.nl/request-headers: "Accept: application/json, Accept-Language: en" + uptime.pdok.nl/response-check-for-string-contains: "200 OK" + uptime.pdok.nl/response-check-for-string-not-contains: "NullPointerException" ``` -**Deploy the Manager to the cluster with the image specified by `IMG`:** +## Run/usage -```sh -make deploy IMG=/uptime-operator:tag +```shell +go build github.com/PDOK/uptime-operator/cmd -o manager ``` -> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin -privileges or be logged in as admin. +or -**Create instances of your solution** -You can apply the samples (examples) from the config/sample: - -```sh -kubectl apply -k config/samples/ -``` - ->**NOTE**: Ensure that the samples has default values to test it out. - -### To Uninstall -**Delete the instances (CRs) from the cluster:** - -```sh -kubectl delete -k config/samples/ +```shell +docker build -t pdok/uptime-operator . ``` -**Delete the APIs(CRDs) from the cluster:** - -```sh -make uninstall +```text +USAGE: + [OPTIONS] + +OPTIONS: + -enable-http2 + If set, HTTP/2 will be enabled for the metrics and webhook servers. + -health-probe-bind-address string + The address the probe endpoint binds to. (default ":8081") + -kubeconfig string + Paths to a kubeconfig. Only required if out-of-cluster. + -leader-elect + Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager. + -metrics-bind-address string + The address the metric endpoint binds to. (default ":8080") + -metrics-secure + If set the metrics endpoint is served securely. + -namespace value + Namespace(s) to watch for changes. Specify this flag multiple times for each namespace to watch. When not provided all namespaces will be watched. + -slack-channel string + The Slack Channel ID for posting updates when uptime checks are mutated. + -slack-token string + The token required to access the given Slack channel. + -uptime-provider string + Name of the (SaaS) uptime monitoring provider to use. (default "mock") + -zap-devel + Development Mode defaults(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn). Production Mode defaults(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error) (default true) + -zap-encoder value + Zap log encoding (one of 'json' or 'console') + -zap-log-level value + Zap Level to configure the verbosity of logging. Can be one of 'debug', 'info', 'error', or any integer value > 0 which corresponds to custom debug levels of increasing verbosity + -zap-stacktrace-level value + Zap Level at and above which stacktraces are captured (one of 'info', 'error', 'panic'). + -zap-time-encoding value + Zap time encoding (one of 'epoch', 'millis', 'nano', 'iso8601', 'rfc3339' or 'rfc3339nano'). Defaults to 'epoch'. ``` -**UnDeploy the controller from the cluster:** - -```sh -make undeploy -``` - -## Project Distribution - -Following are the steps to build the installer and distribute this project to users. - -1. Build the installer for the image built and published in the registry: - -```sh -make build-installer IMG=/uptime-operator:tag -``` - -NOTE: The makefile target mentioned above generates an 'install.yaml' -file in the dist directory. This file contains all the resources built -with Kustomize, which are necessary to install this project without -its dependencies. - -2. Using the installer - -Users can just run kubectl apply -f to install the project, i.e.: - -```sh -kubectl apply -f https://raw.githubusercontent.com//uptime-operator//dist/install.yaml -``` - -## Contributing -// TODO(user): Add detailed information on how you would like others to contribute to this project - -**NOTE:** Run `make help` for more information on all potential `make` targets - -More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) - ## License -``` +```text MIT License Copyright (c) 2024 Publieke Dienstverlening op de Kaart diff --git a/cmd/main.go b/cmd/main.go index 109c902..f6f32e0 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -67,6 +67,7 @@ func main() { var namespaces util.SliceFlag var slackChannel string var slackToken string + var uptimeProvider string flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, @@ -80,6 +81,7 @@ func main() { "Specify this flag multiple times for each namespace to watch. When not provided all namespaces will be watched.") flag.StringVar(&slackChannel, "slack-channel", "", "The Slack Channel ID for posting updates when uptime checks are mutated.") flag.StringVar(&slackToken, "slack-token", "", "The token required to access the given Slack channel.") + flag.StringVar(&uptimeProvider, "uptime-provider", "mock", "Name of the (SaaS) uptime monitoring provider to use.") opts := zap.Options{ Development: true, } @@ -149,7 +151,7 @@ func main() { if err = (&controller.IngressRouteReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - UptimeCheckService: service.New("mock", slackToken, slackChannel), + UptimeCheckService: service.New(uptimeProvider, slackToken, slackChannel), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "IngressRoute") os.Exit(1) diff --git a/internal/service/service.go b/internal/service/service.go index a39f63f..5c1448d 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -16,7 +16,7 @@ type UptimeCheckService struct { func New(provider string, slackToken string, slackChannel string) *UptimeCheckService { var p UptimeProvider - switch provider { + switch provider { //nolint:gocritic case "mock": p = providers.NewMockUptimeProvider() // TODO add new case(s) for actual uptime monitoring SaaS providers @@ -26,7 +26,6 @@ func New(provider string, slackToken string, slackChannel string) *UptimeCheckSe if slackToken != "" && slackChannel != "" { slack = NewSlack(slackToken, slackChannel) } - return &UptimeCheckService{ slack: slack, provider: p, @@ -36,11 +35,10 @@ func New(provider string, slackToken string, slackChannel string) *UptimeCheckSe func (r *UptimeCheckService) Mutate(ctx context.Context, mutation m.Mutation, annotations map[string]string) { check := m.NewUptimeCheck(annotations) if check != nil { - switch mutation { - case m.CreateOrUpdate: + if mutation == m.CreateOrUpdate { err := r.provider.CreateOrUpdateCheck(*check) r.logMutation(ctx, err, mutation, check) - case m.Delete: + } else if mutation == m.Delete { err := r.provider.DeleteCheck(*check) r.logMutation(ctx, err, mutation, check) } @@ -51,19 +49,20 @@ func (r *UptimeCheckService) logMutation(ctx context.Context, err error, mutatio if err != nil { msg := fmt.Sprintf("%s of uptime check '%s' (id: %s) failed", string(mutation), check.Name, check.ID) log.FromContext(ctx).Error(err, msg, "check", check) - if r.slack != nil { - r.slack.SendSlackMessage(ctx, ":large_red_square: "+msg) + if r.slack == nil { + return } - + r.slack.SendSlackMessage(ctx, ":large_red_square: "+msg) } else { msg := fmt.Sprintf("%s of uptime check '%s' (id: %s) succeeded", string(mutation), check.Name, check.ID) log.FromContext(ctx).Info(msg) - if r.slack != nil { - emoji := ":large_green_square: " - if mutation == m.Delete { - emoji = ":warning: " - } - r.slack.SendSlackMessage(ctx, emoji+msg) + if r.slack == nil { + return + } + if mutation == m.Delete { + r.slack.SendSlackMessage(ctx, ":warning: "+msg+". Beware: a flood of these delete message may indicate Traefik itself is down!") + } else { + r.slack.SendSlackMessage(ctx, ":large_green_square: "+msg) } } } From b03106175abf32a811afa8bdf3348806096c1cb4 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 16 Apr 2024 21:21:45 +0200 Subject: [PATCH 17/26] chore(build): fix lint + readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 214fee9..f2c0176 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # uptime-operator -Kubernetes Operator to watch Traefik IngressRoute(s) and register these with a (SaaS) uptime monitoring provider to watch availability. +Kubernetes Operator to watch Traefik IngressRoute(s) and register these with a (SaaS) uptime monitoring provider. ## Annotations From e815198d7e5e6dfddc7cdcb0a38d21afd5733979 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 16 Apr 2024 21:24:28 +0200 Subject: [PATCH 18/26] chore(build): readme --- README.md | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f2c0176..e35bd36 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # uptime-operator -Kubernetes Operator to watch Traefik IngressRoute(s) and register these with a (SaaS) uptime monitoring provider. +Kubernetes Operator to watch [Traefik](https://github.com/traefik/traefik) IngressRoute(s) and register these with a (SaaS) uptime monitoring provider. ## Annotations -Traefik `IngressRoute` resource should be annotated in order to successfully register an uptime check. For example: +Traefik `IngressRoute` resources should be annotated in order to successfully register an uptime check. For example: ```yaml apiVersion: traefik.containo.us/v1alpha1 @@ -21,6 +21,8 @@ metadata: uptime.pdok.nl/response-check-for-string-not-contains: "NullPointerException" ``` +Both `traefik.containo.us/v1alpha1` as well as `traefik.io/v1alpha1` resources are supported. + ## Run/usage ```shell @@ -70,6 +72,34 @@ OPTIONS: Zap time encoding (one of 'epoch', 'millis', 'nano', 'iso8601', 'rfc3339' or 'rfc3339nano'). Defaults to 'epoch'. ``` +## Develop + +The project is written in Go and scaffolded with [kubebuilder](https://kubebuilder.io). + +### kubebuilder + +This operator was scaffolded with [kubebuilder](https://kubebuilder.io) + +Read the manual when you want/need to make changes. +E.g. run `make test` before committing. + +### Linting + +Install [golangci-lint](https://golangci-lint.run/usage/install/) and run `golangci-lint run` +from the root. +(Don't run `make lint`, it uses an old version of golangci-lint.) + +## Misc + +### How to Contribute + +Make a pull request... + +### Contact + +Contacting the maintainers can be done through the issue tracker. + + ## License ```text From 169f109990556540dad33421600ba4c69e22655d Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 16 Apr 2024 21:44:53 +0200 Subject: [PATCH 19/26] feat(uptime): validate uptime checks --- .../controller/ingressroute_controller.go | 4 +- internal/model/check.go | 19 ++++++--- internal/service/service.go | 41 ++++++++++++------- internal/service/slack.go | 2 +- 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/internal/controller/ingressroute_controller.go b/internal/controller/ingressroute_controller.go index b55cd3b..afb9552 100644 --- a/internal/controller/ingressroute_controller.go +++ b/internal/controller/ingressroute_controller.go @@ -65,13 +65,13 @@ func (r *IngressRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, client.IgnoreNotFound(err) } shouldContinue, err := finalizeIfNecessary(ctx, r.Client, ingressRoute, m.AnnotationFinalizer, func() error { - r.UptimeCheckService.Mutate(ctx, m.Delete, ingressRoute.GetAnnotations()) + r.UptimeCheckService.Mutate(ctx, m.Delete, ingressRoute.GetName(), ingressRoute.GetAnnotations()) return nil }) if !shouldContinue || err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } - r.UptimeCheckService.Mutate(ctx, m.CreateOrUpdate, ingressRoute.GetAnnotations()) + r.UptimeCheckService.Mutate(ctx, m.CreateOrUpdate, ingressRoute.GetName(), ingressRoute.GetAnnotations()) return ctrl.Result{}, nil } diff --git a/internal/model/check.go b/internal/model/check.go index e4475d5..fc995e9 100644 --- a/internal/model/check.go +++ b/internal/model/check.go @@ -1,6 +1,7 @@ package model import ( + "fmt" "slices" "strings" ) @@ -32,15 +33,23 @@ type UptimeCheck struct { StringNotContains string `json:"string_not_contains"` } -func NewUptimeCheck(annotations map[string]string) *UptimeCheck { +func NewUptimeCheck(k8sObjName string, annotations map[string]string) (*UptimeCheck, error) { id, ok := annotations[annotationID] if !ok { - return nil + return nil, fmt.Errorf("id annotation not found on ingress route: %s", k8sObjName) + } + name, ok := annotations[annotationName] + if !ok { + return nil, fmt.Errorf("name annotation not found on ingress route: %s", k8sObjName) + } + url, ok := annotations[annotationURL] + if !ok { + return nil, fmt.Errorf("url annotation not found on ingress route %s", k8sObjName) } check := &UptimeCheck{ ID: id, - Name: annotations[annotationName], - URL: annotations[annotationURL], + Name: name, + URL: url, Tags: stringToSlice(annotations[annotationTags]), RequestHeaders: kvStringToMap(annotations[annotationRequestHeaders]), StringContains: annotations[annotationStringContains], @@ -49,7 +58,7 @@ func NewUptimeCheck(annotations map[string]string) *UptimeCheck { if !slices.Contains(check.Tags, tagManagedBy) { check.Tags = append(check.Tags, tagManagedBy) } - return check + return check, nil } func kvStringToMap(s string) map[string]string { diff --git a/internal/service/service.go b/internal/service/service.go index 5c1448d..cc95fa5 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -32,37 +32,48 @@ func New(provider string, slackToken string, slackChannel string) *UptimeCheckSe } } -func (r *UptimeCheckService) Mutate(ctx context.Context, mutation m.Mutation, annotations map[string]string) { - check := m.NewUptimeCheck(annotations) - if check != nil { - if mutation == m.CreateOrUpdate { - err := r.provider.CreateOrUpdateCheck(*check) - r.logMutation(ctx, err, mutation, check) - } else if mutation == m.Delete { - err := r.provider.DeleteCheck(*check) - r.logMutation(ctx, err, mutation, check) - } +func (r *UptimeCheckService) Mutate(ctx context.Context, mutation m.Mutation, k8sObjName string, annotations map[string]string) { + check, err := m.NewUptimeCheck(k8sObjName, annotations) + if err != nil { + r.logAnnotationErr(ctx, err) + return + } + if mutation == m.CreateOrUpdate { + err = r.provider.CreateOrUpdateCheck(*check) + r.logMutation(ctx, err, mutation, check) + } else if mutation == m.Delete { + err = r.provider.DeleteCheck(*check) + r.logMutation(ctx, err, mutation, check) + } +} + +func (r *UptimeCheckService) logAnnotationErr(ctx context.Context, err error) { + msg := fmt.Sprintf("missing or invalid uptime check annotation(s) encountered: %v", err) + log.FromContext(ctx).Error(err, msg) + if r.slack == nil { + return } + r.slack.Send(ctx, ":large_red_square: "+msg) } func (r *UptimeCheckService) logMutation(ctx context.Context, err error, mutation m.Mutation, check *m.UptimeCheck) { if err != nil { - msg := fmt.Sprintf("%s of uptime check '%s' (id: %s) failed", string(mutation), check.Name, check.ID) + msg := fmt.Sprintf("%s of uptime check '%s' (id: %s) failed.", string(mutation), check.Name, check.ID) log.FromContext(ctx).Error(err, msg, "check", check) if r.slack == nil { return } - r.slack.SendSlackMessage(ctx, ":large_red_square: "+msg) + r.slack.Send(ctx, ":large_red_square: "+msg) } else { - msg := fmt.Sprintf("%s of uptime check '%s' (id: %s) succeeded", string(mutation), check.Name, check.ID) + msg := fmt.Sprintf("%s of uptime check '%s' (id: %s) succeeded.", string(mutation), check.Name, check.ID) log.FromContext(ctx).Info(msg) if r.slack == nil { return } if mutation == m.Delete { - r.slack.SendSlackMessage(ctx, ":warning: "+msg+". Beware: a flood of these delete message may indicate Traefik itself is down!") + r.slack.Send(ctx, ":warning: "+msg+".\n_Beware: a flood of these delete messages may indicate Traefik itself is down!_") } else { - r.slack.SendSlackMessage(ctx, ":large_green_square: "+msg) + r.slack.Send(ctx, ":large_green_square: "+msg) } } } diff --git a/internal/service/slack.go b/internal/service/slack.go index 7c2e139..3a14296 100644 --- a/internal/service/slack.go +++ b/internal/service/slack.go @@ -20,7 +20,7 @@ func NewSlack(token, channelID string) *Slack { } } -func (s *Slack) SendSlackMessage(ctx context.Context, message string) { +func (s *Slack) Send(ctx context.Context, message string) { logger := log.FromContext(ctx) channelID, timestamp, err := s.client.PostMessageContext(ctx, s.channelID, slack.MsgOptionText(message, false), From 5f936bf760e29e192270a07221d81134f7d63c7d Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 16 Apr 2024 22:00:15 +0200 Subject: [PATCH 20/26] chore(docker): fix Dockerfile --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8ed9335..f1cf0fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,8 +13,7 @@ RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go -COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command From 4a3588fed346ac4905be1f7275b6c56b0d3ce480 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Wed, 17 Apr 2024 09:45:22 +0200 Subject: [PATCH 21/26] test(uptime): Add unit test --- internal/model/check.go | 11 ++-- internal/model/check_test.go | 102 +++++++++++++++++++++++++++++++++++ internal/service/service.go | 24 ++++----- internal/service/slack.go | 2 +- 4 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 internal/model/check_test.go diff --git a/internal/model/check.go b/internal/model/check.go index fc995e9..9bad595 100644 --- a/internal/model/check.go +++ b/internal/model/check.go @@ -33,18 +33,18 @@ type UptimeCheck struct { StringNotContains string `json:"string_not_contains"` } -func NewUptimeCheck(k8sObjName string, annotations map[string]string) (*UptimeCheck, error) { +func NewUptimeCheck(ingressName string, annotations map[string]string) (*UptimeCheck, error) { id, ok := annotations[annotationID] if !ok { - return nil, fmt.Errorf("id annotation not found on ingress route: %s", k8sObjName) + return nil, fmt.Errorf("%s annotation not found on ingress route: %s", annotationID, ingressName) } name, ok := annotations[annotationName] if !ok { - return nil, fmt.Errorf("name annotation not found on ingress route: %s", k8sObjName) + return nil, fmt.Errorf("%s annotation not found on ingress route: %s", annotationName, ingressName) } url, ok := annotations[annotationURL] if !ok { - return nil, fmt.Errorf("url annotation not found on ingress route %s", k8sObjName) + return nil, fmt.Errorf("%s annotation not found on ingress route %s", annotationURL, ingressName) } check := &UptimeCheck{ ID: id, @@ -66,6 +66,9 @@ func kvStringToMap(s string) map[string]string { result := make(map[string]string) for _, kvPair := range kvPairs { parts := strings.Split(kvPair, ":") + if len(parts) != 2 { + continue + } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) result[key] = value diff --git a/internal/model/check_test.go b/internal/model/check_test.go new file mode 100644 index 0000000..06f05ee --- /dev/null +++ b/internal/model/check_test.go @@ -0,0 +1,102 @@ +package model + +import ( + "testing" +) + +func TestNewUptimeCheck(t *testing.T) { + tests := []struct { + name string + ingressName string + annotations map[string]string + wantErr bool + }{ + { + name: "All annotations present", + ingressName: "test-ingress", + annotations: map[string]string{ + "uptime.pdok.nl/id": "1234567890", + "uptime.pdok.nl/name": "Test Check", + "uptime.pdok.nl/url": "https://pdok.example", + "uptime.pdok.nl/tags": "tag1, tag2", + "uptime.pdok.nl/request-headers": "key1:value1, key2:value2", + "uptime.pdok.nl/response-check-contains": "test string", + "uptime.pdok.nl/response-check-not-contains": "", + }, + wantErr: false, + }, + { + name: "Missing ID annotation", + ingressName: "test-ingress", + annotations: map[string]string{ + "uptime.pdok.nl/name": "Test Check", + "uptime.pdok.nl/url": "https://pdok.example", + "uptime.pdok.nl/tags": "tag1, tag2", + "uptime.pdok.nl/request-headers": "key1:value1, key2:value2", + "uptime.pdok.nl/response-check-contains": "test string", + "uptime.pdok.nl/response-check-not-contains": "", + }, + wantErr: true, + }, + { + name: "Missing Name annotation", + ingressName: "test-ingress", + annotations: map[string]string{ + "uptime.pdok.nl/id": "1234567890", + "uptime.pdok.nl/url": "https://pdok.example", + "uptime.pdok.nl/tags": "tag1, tag2", + "uptime.pdok.nl/request-headers": "key1:value1, key2:value2", + "uptime.pdok.nl/response-check-contains": "test string", + "uptime.pdok.nl/response-check-not-contains": "", + }, + wantErr: true, + }, + { + name: "Missing URL annotation", + ingressName: "test-ingress", + annotations: map[string]string{ + "uptime.pdok.nl/id": "1234567890", + "uptime.pdok.nl/name": "Test Check", + "uptime.pdok.nl/tags": "tag1, tag2", + "uptime.pdok.nl/request-headers": "key1:value1, key2:value2", + "uptime.pdok.nl/response-check-contains": "test string", + "uptime.pdok.nl/response-check-not-contains": "", + }, + wantErr: true, + }, + { + name: "Missing tags annotation", + ingressName: "test-ingress", + annotations: map[string]string{ + "uptime.pdok.nl/id": "1234567890", + "uptime.pdok.nl/name": "Test Check", + "uptime.pdok.nl/url": "https://pdok.example", + "uptime.pdok.nl/request-headers": "key1:value1, key2:value2", + "uptime.pdok.nl/response-check-contains": "test string", + "uptime.pdok.nl/response-check-not-contains": "", + }, + wantErr: false, + }, + { + name: "Missing request-headers annotation", + ingressName: "test-ingress", + annotations: map[string]string{ + "uptime.pdok.nl/id": "1234567890", + "uptime.pdok.nl/name": "Test Check", + "uptime.pdok.nl/url": "https://pdok.example", + "uptime.pdok.nl/tags": "tag1, tag2", + "uptime.pdok.nl/response-check-contains": "test string", + "uptime.pdok.nl/response-check-not-contains": "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewUptimeCheck(tt.ingressName, tt.annotations) + if (err != nil) != tt.wantErr { + t.Errorf("NewUptimeCheck() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/service/service.go b/internal/service/service.go index cc95fa5..520a966 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -32,8 +32,8 @@ func New(provider string, slackToken string, slackChannel string) *UptimeCheckSe } } -func (r *UptimeCheckService) Mutate(ctx context.Context, mutation m.Mutation, k8sObjName string, annotations map[string]string) { - check, err := m.NewUptimeCheck(k8sObjName, annotations) +func (r *UptimeCheckService) Mutate(ctx context.Context, mutation m.Mutation, ingressName string, annotations map[string]string) { + check, err := m.NewUptimeCheck(ingressName, annotations) if err != nil { r.logAnnotationErr(ctx, err) return @@ -64,16 +64,16 @@ func (r *UptimeCheckService) logMutation(ctx context.Context, err error, mutatio return } r.slack.Send(ctx, ":large_red_square: "+msg) + return + } + msg := fmt.Sprintf("%s of uptime check '%s' (id: %s) succeeded.", string(mutation), check.Name, check.ID) + log.FromContext(ctx).Info(msg) + if r.slack == nil { + return + } + if mutation == m.Delete { + r.slack.Send(ctx, ":warning: "+msg+".\n _Beware: a flood of these delete messages may indicate Traefik itself is down!_") } else { - msg := fmt.Sprintf("%s of uptime check '%s' (id: %s) succeeded.", string(mutation), check.Name, check.ID) - log.FromContext(ctx).Info(msg) - if r.slack == nil { - return - } - if mutation == m.Delete { - r.slack.Send(ctx, ":warning: "+msg+".\n_Beware: a flood of these delete messages may indicate Traefik itself is down!_") - } else { - r.slack.Send(ctx, ":large_green_square: "+msg) - } + r.slack.Send(ctx, ":large_green_square: "+msg) } } diff --git a/internal/service/slack.go b/internal/service/slack.go index 3a14296..1ed0a0e 100644 --- a/internal/service/slack.go +++ b/internal/service/slack.go @@ -25,7 +25,7 @@ func (s *Slack) Send(ctx context.Context, message string) { channelID, timestamp, err := s.client.PostMessageContext(ctx, s.channelID, slack.MsgOptionText(message, false), slack.MsgOptionUsername(model.OperatorName), - slack.MsgOptionIconEmoji(":robot_face:"), + slack.MsgOptionIconEmoji(":up:"), ) if err != nil { logger.Error(err, "failed to post Slack message", "message", message, "channel", channelID, "timestamp", timestamp) From 822d808f794c8ce12a17849aa2b1e27acb6cc191 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Thu, 18 Apr 2024 15:12:51 +0200 Subject: [PATCH 22/26] test(uptime): Add integration test for controller --- README.md | 5 +- cmd/main.go | 9 +- .../controller/ingressroute_controller.go | 2 +- .../ingressroute_controller_test.go | 168 +++++++++++++++++- internal/controller/suite_test.go | 53 +++++- internal/model/check.go | 42 +++-- internal/service/provider.go | 6 + internal/service/service.go | 43 +++-- internal/service/slack.go | 4 +- 9 files changed, 286 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index e35bd36..d65b4ac 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ metadata: uptime.pdok.nl/response-check-for-string-not-contains: "NullPointerException" ``` +The `id`, `name` and `url` annotations are mandatory, the rest is optional. + Both `traefik.containo.us/v1alpha1` as well as `traefik.io/v1alpha1` resources are supported. ## Run/usage @@ -78,8 +80,6 @@ The project is written in Go and scaffolded with [kubebuilder](https://kubebuild ### kubebuilder -This operator was scaffolded with [kubebuilder](https://kubebuilder.io) - Read the manual when you want/need to make changes. E.g. run `make test` before committing. @@ -99,7 +99,6 @@ Make a pull request... Contacting the maintainers can be done through the issue tracker. - ## License ```text diff --git a/cmd/main.go b/cmd/main.go index f6f32e0..7cf81b7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -149,9 +149,12 @@ func main() { } if err = (&controller.IngressRouteReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - UptimeCheckService: service.New(uptimeProvider, slackToken, slackChannel), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + UptimeCheckService: service.New( + service.WithProviderName(uptimeProvider), + service.WithSlack(slackToken, slackChannel), + ), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "IngressRoute") os.Exit(1) diff --git a/internal/controller/ingressroute_controller.go b/internal/controller/ingressroute_controller.go index afb9552..c9b38d4 100644 --- a/internal/controller/ingressroute_controller.go +++ b/internal/controller/ingressroute_controller.go @@ -102,7 +102,7 @@ func finalizeIfNecessary(ctx context.Context, c client.Client, obj client.Object if !controllerutil.ContainsFinalizer(obj, finalizerName) { controllerutil.AddFinalizer(obj, finalizerName) err = c.Update(ctx, obj) - return false, err + return true, err } return true, nil } diff --git a/internal/controller/ingressroute_controller_test.go b/internal/controller/ingressroute_controller_test.go index a15df12..97b3ad2 100644 --- a/internal/controller/ingressroute_controller_test.go +++ b/internal/controller/ingressroute_controller_test.go @@ -25,16 +25,176 @@ SOFTWARE. package controller import ( + "context" + "fmt" + + m "github.com/PDOK/uptime-operator/internal/model" + "github.com/PDOK/uptime-operator/internal/service" . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd + . "github.com/onsi/gomega" //nolint:revive // gingko bdd + traefikcontainous "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + testIngress = "test-ingress-resource" + testNamespace = "default" ) +type testUptimeProvider struct { + checks map[string]m.UptimeCheck +} + +func newTestUptimeProvider() *testUptimeProvider { + return &testUptimeProvider{ + checks: make(map[string]m.UptimeCheck), + } +} + +func (m *testUptimeProvider) HasCheck(check m.UptimeCheck) bool { + _, ok := m.checks[check.ID] + return ok +} + +func (m *testUptimeProvider) CreateOrUpdateCheck(check m.UptimeCheck) error { + m.checks[check.ID] = check + return nil +} + +func (m *testUptimeProvider) DeleteCheck(check m.UptimeCheck) error { + delete(m.checks, check.ID) + return nil +} + +var ingressRouteWithUptimeCheck = &traefikcontainous.IngressRoute{ + TypeMeta: v1.TypeMeta{}, + ObjectMeta: v1.ObjectMeta{ + Name: testIngress, + Namespace: testNamespace, + Annotations: map[string]string{ + // with uptime check annotations + m.AnnotationID: "y45735y375", + m.AnnotationURL: "https://test.example", + m.AnnotationName: "Test uptime check", + }, + }, + Spec: traefikcontainous.IngressRouteSpec{ + Routes: []traefikcontainous.Route{ + { + Kind: "Rule", + Match: "Host(`localhost`)", + Services: []traefikcontainous.Service{ + { + LoadBalancerSpec: traefikcontainous.LoadBalancerSpec{ + Name: "test", + }, + }, + }, + }, + }, + }, +} + var _ = Describe("IngressRoute Controller", func() { - Context("When reconciling a resource", func() { + Context("When reconciling IngressRoutes", func() { + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: testIngress, + Namespace: testNamespace, + } + + It("Should successfully create + update an uptime check for an ingress route", func() { + testProvider := newTestUptimeProvider() + controllerReconciler := &IngressRouteReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + UptimeCheckService: service.New(service.WithProvider(testProvider)), + } + + By("Creating an IngressRoute") + newIngressRoute := &traefikcontainous.IngressRoute{} + err := k8sClient.Get(ctx, typeNamespacedName, newIngressRoute) + if err != nil { + if k8serrors.IsNotFound(err) { + resource := ingressRouteWithUptimeCheck.DeepCopy() + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + Expect(k8sClient.Get(ctx, typeNamespacedName, newIngressRoute)).To(Succeed()) + } else { + Fail(fmt.Sprintf("%v", err)) + } + } + + By("Reconciling the IngressRoute (thus creating an uptime check)") + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + Expect(testProvider.checks).To(ContainElement(m.UptimeCheck{ + ID: "y45735y375", + URL: "https://test.example", + Name: "Test uptime check", + Tags: []string{"managed-by-uptime-operator"}, + })) + + By("Fetching and updating IngressRoute (adding extra uptime annotation)") + fetchedIngressRoute := &traefikcontainous.IngressRoute{} + Eventually(func() bool { + err := k8sClient.Get(ctx, typeNamespacedName, fetchedIngressRoute) + return err == nil + }, "10s", "1s").Should(BeTrue()) + fetchedIngressRoute.Annotations[m.AnnotationStringContains] = "OK" + Expect(k8sClient.Update(ctx, fetchedIngressRoute)).Should(Succeed()) + + By("Reconciling the IngressRoute again (to make sure uptime check is updated)") + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + Expect(testProvider.checks).To(ContainElement(m.UptimeCheck{ + ID: "y45735y375", + URL: "https://test.example", + Name: "Test uptime check", + Tags: []string{"managed-by-uptime-operator"}, + StringContains: "OK", + })) + + By("Reconciling the IngressRoute again to make sure it doesn't cause any side effects") + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + Expect(testProvider.checks).To(HaveLen(1)) + }) + + It("Should delete uptime check for an existing ingress route", func() { + testProvider := newTestUptimeProvider() + controllerReconciler := &IngressRouteReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + UptimeCheckService: service.New(service.WithProvider(testProvider)), + } + + By("Reconciling the IngressRoute (expecting on is available from previous test)") + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + Expect(testProvider.checks).To(ContainElement(m.UptimeCheck{ + ID: "y45735y375", + URL: "https://test.example", + Name: "Test uptime check", + Tags: []string{"managed-by-uptime-operator"}, + StringContains: "OK", + })) - It("should successfully reconcile the resource", func() { + By("Delete IngressRoute") + fetchedIngressRoute := &traefikcontainous.IngressRoute{} + Eventually(func() bool { + err := k8sClient.Get(ctx, typeNamespacedName, fetchedIngressRoute) + return err == nil + }, "10s", "1s").Should(BeTrue()) + Expect(k8sClient.Delete(ctx, fetchedIngressRoute)).To(Succeed()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. + By("Reconciling the IngressRoute again (to make sure uptime check is deleted)") + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) + Expect(err).NotTo(HaveOccurred()) + Expect(testProvider.checks).To(BeEmpty()) }) }) }) diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 05f5da4..bd5cd9c 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -25,13 +25,20 @@ SOFTWARE. package controller import ( + "encoding/json" + "errors" "fmt" + "os/exec" "path/filepath" "runtime" "testing" + "golang.org/x/tools/go/packages" + . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd . "github.com/onsi/gomega" //nolint:revive // ginkgo bdd + traefikcontainous "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1" + traefikio "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikio/v1alpha1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" @@ -59,10 +66,16 @@ var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) By("bootstrapping test environment") + traefikCRDPath := must(getTraefikCRDPath()) testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, - ErrorIfCRDPathMissing: false, - + ErrorIfCRDPathMissing: true, + CRDInstallOptions: envtest.CRDInstallOptions{ + Scheme: nil, + Paths: []string{ + traefikCRDPath, + }, + ErrorIfPathMissing: true, + }, // The BinaryAssetsDirectory is only required if you want to run the tests directly // without call the makefile target test. If not informed it will look for the // default path defined in controller-runtime which is /usr/local/kubebuilder/. @@ -78,6 +91,12 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) + err = traefikcontainous.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = traefikio.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + //+kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) @@ -91,3 +110,31 @@ var _ = AfterSuite(func() { err := testEnv.Stop() Expect(err).NotTo(HaveOccurred()) }) + +func getTraefikCRDPath() (string, error) { + traefikModule, err := getModule("github.com/traefik/traefik/v2") + if err != nil { + return "", err + } + if traefikModule.Dir == "" { + return "", errors.New("cannot find path for traefik module") + } + return filepath.Join(traefikModule.Dir, "integration", "fixtures", "k8s", "01-traefik-crd.yml"), nil +} + +func getModule(name string) (module *packages.Module, err error) { + out, err := exec.Command("go", "list", "-json", "-m", name).Output() + if err != nil { + return + } + module = &packages.Module{} + err = json.Unmarshal(out, module) + return +} + +func must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} diff --git a/internal/model/check.go b/internal/model/check.go index 9bad595..270790d 100644 --- a/internal/model/check.go +++ b/internal/model/check.go @@ -14,13 +14,13 @@ const ( AnnotationBase = "uptime.pdok.nl" AnnotationFinalizer = AnnotationBase + "/finalizer" - annotationID = AnnotationBase + "/id" - annotationName = AnnotationBase + "/name" - annotationURL = AnnotationBase + "/url" - annotationTags = AnnotationBase + "/tags" - annotationRequestHeaders = AnnotationBase + "/request-headers" - annotationStringContains = AnnotationBase + "/response-check-for-string-contains" - annotationStringNotContains = AnnotationBase + "/response-check-for-string-not-contains" + AnnotationID = AnnotationBase + "/id" + AnnotationName = AnnotationBase + "/name" + AnnotationURL = AnnotationBase + "/url" + AnnotationTags = AnnotationBase + "/tags" + AnnotationRequestHeaders = AnnotationBase + "/request-headers" + AnnotationStringContains = AnnotationBase + "/response-check-for-string-contains" + AnnotationStringNotContains = AnnotationBase + "/response-check-for-string-not-contains" ) type UptimeCheck struct { @@ -34,26 +34,26 @@ type UptimeCheck struct { } func NewUptimeCheck(ingressName string, annotations map[string]string) (*UptimeCheck, error) { - id, ok := annotations[annotationID] + id, ok := annotations[AnnotationID] if !ok { - return nil, fmt.Errorf("%s annotation not found on ingress route: %s", annotationID, ingressName) + return nil, fmt.Errorf("%s annotation not found on ingress route: %s", AnnotationID, ingressName) } - name, ok := annotations[annotationName] + name, ok := annotations[AnnotationName] if !ok { - return nil, fmt.Errorf("%s annotation not found on ingress route: %s", annotationName, ingressName) + return nil, fmt.Errorf("%s annotation not found on ingress route: %s", AnnotationName, ingressName) } - url, ok := annotations[annotationURL] + url, ok := annotations[AnnotationURL] if !ok { - return nil, fmt.Errorf("%s annotation not found on ingress route %s", annotationURL, ingressName) + return nil, fmt.Errorf("%s annotation not found on ingress route %s", AnnotationURL, ingressName) } check := &UptimeCheck{ ID: id, Name: name, URL: url, - Tags: stringToSlice(annotations[annotationTags]), - RequestHeaders: kvStringToMap(annotations[annotationRequestHeaders]), - StringContains: annotations[annotationStringContains], - StringNotContains: annotations[annotationStringNotContains], + Tags: stringToSlice(annotations[AnnotationTags]), + RequestHeaders: kvStringToMap(annotations[AnnotationRequestHeaders]), + StringContains: annotations[AnnotationStringContains], + StringNotContains: annotations[AnnotationStringNotContains], } if !slices.Contains(check.Tags, tagManagedBy) { check.Tags = append(check.Tags, tagManagedBy) @@ -62,8 +62,11 @@ func NewUptimeCheck(ingressName string, annotations map[string]string) (*UptimeC } func kvStringToMap(s string) map[string]string { - kvPairs := strings.Split(s, ",") + if s == "" { + return nil + } result := make(map[string]string) + kvPairs := strings.Split(s, ",") for _, kvPair := range kvPairs { parts := strings.Split(kvPair, ":") if len(parts) != 2 { @@ -77,6 +80,9 @@ func kvStringToMap(s string) map[string]string { } func stringToSlice(s string) []string { + if s == "" { + return nil + } var result []string splits := strings.Split(s, ",") for _, part := range splits { diff --git a/internal/service/provider.go b/internal/service/provider.go index fde8047..6568a7d 100644 --- a/internal/service/provider.go +++ b/internal/service/provider.go @@ -5,7 +5,13 @@ import ( ) type UptimeProvider interface { + // HasCheck true when the check with the given ID exists, false otherwise HasCheck(check model.UptimeCheck) bool + + // CreateOrUpdateCheck create the given check with the uptime monitoring + // provider, or update an existing check. Needs to be idempotent! CreateOrUpdateCheck(check model.UptimeCheck) error + + // DeleteCheck deletes the given check with from the uptime monitoring provider DeleteCheck(check model.UptimeCheck) error } diff --git a/internal/service/service.go b/internal/service/service.go index 520a966..38d5ea8 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -9,26 +9,45 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) +type UptimeCheckOption func(*UptimeCheckService) *UptimeCheckService + type UptimeCheckService struct { provider UptimeProvider slack *Slack } -func New(provider string, slackToken string, slackChannel string) *UptimeCheckService { - var p UptimeProvider - switch provider { //nolint:gocritic - case "mock": - p = providers.NewMockUptimeProvider() - // TODO add new case(s) for actual uptime monitoring SaaS providers +func New(options ...UptimeCheckOption) *UptimeCheckService { + service := &UptimeCheckService{} + for _, option := range options { + service = option(service) } + return service +} - var slack *Slack - if slackToken != "" && slackChannel != "" { - slack = NewSlack(slackToken, slackChannel) +func WithProvider(provider UptimeProvider) UptimeCheckOption { + return func(service *UptimeCheckService) *UptimeCheckService { + service.provider = provider + return service } - return &UptimeCheckService{ - slack: slack, - provider: p, +} + +func WithProviderName(provider string) UptimeCheckOption { + return func(service *UptimeCheckService) *UptimeCheckService { + switch provider { //nolint:gocritic + case "mock": + service.provider = providers.NewMockUptimeProvider() + // TODO add new case(s) for actual uptime monitoring SaaS providers + } + return service + } +} + +func WithSlack(slackToken string, slackChannel string) UptimeCheckOption { + return func(service *UptimeCheckService) *UptimeCheckService { + if slackToken != "" && slackChannel != "" { + service.slack = NewSlack(slackToken, slackChannel) + } + return service } } diff --git a/internal/service/slack.go b/internal/service/slack.go index 1ed0a0e..e47adf1 100644 --- a/internal/service/slack.go +++ b/internal/service/slack.go @@ -21,13 +21,13 @@ func NewSlack(token, channelID string) *Slack { } func (s *Slack) Send(ctx context.Context, message string) { - logger := log.FromContext(ctx) channelID, timestamp, err := s.client.PostMessageContext(ctx, s.channelID, slack.MsgOptionText(message, false), slack.MsgOptionUsername(model.OperatorName), slack.MsgOptionIconEmoji(":up:"), ) if err != nil { - logger.Error(err, "failed to post Slack message", "message", message, "channel", channelID, "timestamp", timestamp) + log.FromContext(ctx).Error(err, "failed to post Slack message", + "message", message, "channel", channelID, "timestamp", timestamp) } } From f185f6d108d58e06cc7acd9c222ee4d08aade5b1 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Thu, 18 Apr 2024 15:17:22 +0200 Subject: [PATCH 23/26] test(uptime): Organize imports --- internal/controller/ingressroute_controller_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/ingressroute_controller_test.go b/internal/controller/ingressroute_controller_test.go index 97b3ad2..c78f5b9 100644 --- a/internal/controller/ingressroute_controller_test.go +++ b/internal/controller/ingressroute_controller_test.go @@ -34,7 +34,7 @@ import ( . "github.com/onsi/gomega" //nolint:revive // gingko bdd traefikcontainous "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1" k8serrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) From deb8f6882323de434042ee517b10d57a8a6a6e08 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Thu, 18 Apr 2024 15:34:45 +0200 Subject: [PATCH 24/26] chore(mod): Tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 1af3cc3..88dccf2 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/onsi/gomega v1.32.0 github.com/slack-go/slack v0.12.5 github.com/traefik/traefik/v2 v2.11.0 + golang.org/x/tools v0.17.0 k8s.io/apimachinery v0.29.3 k8s.io/client-go v0.29.3 sigs.k8s.io/controller-runtime v0.17.3 @@ -69,7 +70,6 @@ require ( golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.17.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect From 78e15e49b8221f2647facd81acb2e4b8c6c3a639 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Thu, 18 Apr 2024 15:50:57 +0200 Subject: [PATCH 25/26] chore(build): Add badges + filter coverage --- .github/workflows/test-go.yml | 2 ++ README.md | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml index e57035f..9532007 100644 --- a/.github/workflows/test-go.yml +++ b/.github/workflows/test-go.yml @@ -21,6 +21,8 @@ jobs: - name: Make test run: | make test + echo "removing generated code from coverage results" + mv cover.out cover.out.tmp && grep -vP "uptime-operator/(api/v1alpha1|cmd|test/utils)/" cover.out.tmp > cover.out - name: Update coverage report uses: ncruces/go-coverage-report@v0 diff --git a/README.md b/README.md index d65b4ac..5c6d1f5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # uptime-operator +[![Build](https://github.com/PDOK/uptime-operator/actions/workflows/build-and-publish-image.yml/badge.svg)](https://github.com/PDOK/uptime-operator/actions/workflows/build-and-publish-image.yml) +[![Lint (go)](https://github.com/PDOK/uptime-operator/actions/workflows/lint-go.yml/badge.svg)](https://github.com/PDOK/uptime-operator/actions/workflows/lint-go.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/PDOK/uptime-operator)](https://goreportcard.com/report/github.com/PDOK/uptime-operator) +[![Coverage (go)](https://github.com/PDOK/uptime-operator/wiki/coverage.svg)](https://raw.githack.com/wiki/PDOK/uptime-operator/coverage.html) +[![GitHub license](https://img.shields.io/github/license/PDOK/uptime-operator)](https://github.com/PDOK/uptime-operator/blob/master/LICENSE) +[![Docker Pulls](https://img.shields.io/docker/pulls/pdok/uptime-operator.svg)](https://hub.docker.com/r/pdok/uptime-operator) + Kubernetes Operator to watch [Traefik](https://github.com/traefik/traefik) IngressRoute(s) and register these with a (SaaS) uptime monitoring provider. ## Annotations From d13eb66a9a989f8d2019950d3e3b6a5d17845460 Mon Sep 17 00:00:00 2001 From: Richard Kettelerij Date: Tue, 23 Apr 2024 17:31:11 +0200 Subject: [PATCH 26/26] review(flags): Allow flags as env vars --- cmd/main.go | 8 +++++++- go.mod | 1 + go.sum | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/cmd/main.go b/cmd/main.go index 7cf81b7..2448b34 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -23,6 +23,7 @@ import ( "github.com/PDOK/uptime-operator/internal/service" "github.com/PDOK/uptime-operator/internal/util" + "github.com/peterbourgon/ff" "sigs.k8s.io/controller-runtime/pkg/cache" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) @@ -82,11 +83,16 @@ func main() { flag.StringVar(&slackChannel, "slack-channel", "", "The Slack Channel ID for posting updates when uptime checks are mutated.") flag.StringVar(&slackToken, "slack-token", "", "The token required to access the given Slack channel.") flag.StringVar(&uptimeProvider, "uptime-provider", "mock", "Name of the (SaaS) uptime monitoring provider to use.") + opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) - flag.Parse() + + if err := ff.Parse(flag.CommandLine, os.Args[1:], ff.WithEnvVarNoPrefix()); err != nil { + setupLog.Error(err, "unable to parse flags") + os.Exit(1) + } ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) diff --git a/go.mod b/go.mod index 88dccf2..47eb28d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22 require ( github.com/onsi/ginkgo/v2 v2.17.1 github.com/onsi/gomega v1.32.0 + github.com/peterbourgon/ff v1.7.1 github.com/slack-go/slack v0.12.5 github.com/traefik/traefik/v2 v2.11.0 golang.org/x/tools v0.17.0 diff --git a/go.sum b/go.sum index 72e5f7a..f59d9cb 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -85,6 +86,7 @@ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvls github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -98,6 +100,9 @@ github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/peterbourgon/ff v1.7.1 h1:xt1lxTG+Nr2+tFtysY7abFgPoH3Lug8CwYJMOmJRXhk= +github.com/peterbourgon/ff v1.7.1/go.mod h1:fYI5YA+3RDqQRExmFbHnBjEeWzh9TrS8rnRpEq7XIg0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -194,6 +199,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= @@ -205,6 +212,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=