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..9532007 --- /dev/null +++ b/.github/workflows/test-go.yml @@ -0,0 +1,37 @@ +--- +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 + + - 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..f1cf0fb 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 @@ -13,15 +13,14 @@ 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 # 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/PROJECT b/PROJECT index 6b3e951..71dde19 100644 --- a/PROJECT +++ b/PROJECT @@ -7,4 +7,15 @@ 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 +- controller: true + domain: pdok.nl + group: traefik.io + kind: IngressRoute + version: v1alpha1 version: "3" diff --git a/README.md b/README.md index 8361be5..5c6d1f5 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,135 @@ # 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 +[![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 + +Traefik `IngressRoute` resources 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" ``` -**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 -``` - -**Deploy the Manager to the cluster with the image specified by `IMG`:** - -```sh -make deploy IMG=/uptime-operator:tag -``` +The `id`, `name` and `url` annotations are mandatory, the rest is optional. -> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin -privileges or be logged in as admin. +Both `traefik.containo.us/v1alpha1` as well as `traefik.io/v1alpha1` resources are supported. -**Create instances of your solution** -You can apply the samples (examples) from the config/sample: +## Run/usage -```sh -kubectl apply -k config/samples/ +```shell +go build github.com/PDOK/uptime-operator/cmd -o manager ``` ->**NOTE**: Ensure that the samples has default values to test it out. - -### To Uninstall -**Delete the instances (CRs) from the cluster:** +or -```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:** +## Develop -```sh -make undeploy -``` +The project is written in Go and scaffolded with [kubebuilder](https://kubebuilder.io). -## Project Distribution +### kubebuilder -Following are the steps to build the installer and distribute this project to users. +Read the manual when you want/need to make changes. +E.g. run `make test` before committing. -1. Build the installer for the image built and published in the registry: +### Linting -```sh -make build-installer IMG=/uptime-operator:tag -``` +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.) -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. +## Misc -2. Using the installer +### How to Contribute -Users can just run kubectl apply -f to install the project, i.e.: +Make a pull request... -```sh -kubectl apply -f https://raw.githubusercontent.com//uptime-operator//dist/install.yaml -``` +### Contact -## Contributing -// TODO(user): Add detailed information on how you would like others to contribute to this project +Contacting the maintainers can be done through the issue tracker. -**NOTE:** Run `make help` for more information on all potential `make` targets +## License -More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) +```text +MIT License -## License +Copyright (c) 2024 Publieke Dienstverlening op de Kaart -Copyright 2024 pdok.nl. +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: -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 +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. - http://www.apache.org/licenses/LICENSE-2.0 +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 7c8fcc4..2448b34 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -21,6 +21,11 @@ import ( "flag" "os" + "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.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -33,6 +38,10 @@ 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" + traefikcontainous "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1" + traefikio "github.com/traefik/traefik/v2/pkg/provider/kubernetes/crd/traefikio/v1alpha1" //+kubebuilder:scaffold:imports ) @@ -44,29 +53,46 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(traefikcontainous.AddToScheme(scheme)) + utilruntime.Must(traefikio.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } +//nolint:funlen func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string var secureMetrics bool var enableHTTP2 bool + 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, "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.") + 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))) @@ -81,7 +107,7 @@ func main() { c.NextProtos = []string{"http/1.1"} } - tlsOpts := []func(*tls.Config){} + var tlsOpts []func(*tls.Config) if !enableHTTP2 { tlsOpts = append(tlsOpts, disableHTTP2) } @@ -90,7 +116,7 @@ func main() { TLSOpts: tlsOpts, }) - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + managerOpts := ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{ BindAddress: metricsAddr, @@ -112,12 +138,33 @@ 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) } + if err = (&controller.IngressRouteReconciler{ + 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) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index bc070bb..2f4c0c0 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -1,15 +1,34 @@ +--- 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: + - get + - list + - watch +- apiGroups: + - traefik.containo.us + resources: + - ingressroutes/finalizers + verbs: + - update +- apiGroups: + - traefik.io + resources: + - ingressroutes + verbs: + - get + - list + - watch +- apiGroups: + - traefik.io + resources: + - ingressroutes/finalizers + verbs: + - update diff --git a/go.mod b/go.mod index 5eb860f..47eb28d 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,31 @@ module github.com/PDOK/uptime-operator -go 1.21 +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/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 + 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 +34,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/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 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/sys v0.16.0 // indirect - golang.org/x/term v0.15.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.24.0 // indirect + golang.org/x/oauth2 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.3.0 // indirect - golang.org/x/tools v0.16.1 // indirect + golang.org/x/time v0.5.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..f59d9cb 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,17 @@ +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= 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 +20,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= @@ -30,30 +37,36 @@ 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= 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.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= 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/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= 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 +84,9 @@ 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/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= @@ -78,14 +94,20 @@ 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/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 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,18 +118,28 @@ 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/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= 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,86 +149,94 @@ 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.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= 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.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= 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.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/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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= 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= 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= 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= 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= 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/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 diff --git a/internal/controller/ingressroute_controller.go b/internal/controller/ingressroute_controller.go new file mode 100644 index 0000000..c9b38d4 --- /dev/null +++ b/internal/controller/ingressroute_controller.go @@ -0,0 +1,139 @@ +/* +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" + + 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" + "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/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// IngressRouteReconciler reconciles Traefik IngressRoutes with an uptime monitoring (SaaS) provider +type IngressRouteReconciler struct { + client.Client + Scheme *runtime.Scheme + UptimeCheckService *service.UptimeCheckService +} + +//+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 +//+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. +// +// 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) { + ingressRoute, err := r.getIngressRoute(ctx, req) + if err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + shouldContinue, err := finalizeIfNecessary(ctx, r.Client, ingressRoute, m.AnnotationFinalizer, func() error { + 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.GetName(), ingressRoute.GetAnnotations()) + return ctrl.Result{}, nil +} + +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 try getting "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 + } + return ingressIo, nil + } + return ingressContainous, 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 true, 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 +} + +// 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(m.OperatorName). + 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/controller/ingressroute_controller_test.go b/internal/controller/ingressroute_controller_test.go new file mode 100644 index 0000000..c78f5b9 --- /dev/null +++ b/internal/controller/ingressroute_controller_test.go @@ -0,0 +1,200 @@ +/* +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" + "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" + v1 "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 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", + })) + + 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()) + + 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 new file mode 100644 index 0000000..bd5cd9c --- /dev/null +++ b/internal/controller/suite_test.go @@ -0,0 +1,140 @@ +/* +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 ( + "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" + "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") + traefikCRDPath := must(getTraefikCRDPath()) + testEnv = &envtest.Environment{ + 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/. + // 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()) + + 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}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + 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 new file mode 100644 index 0000000..270790d --- /dev/null +++ b/internal/model/check.go @@ -0,0 +1,92 @@ +package model + +import ( + "fmt" + "slices" + "strings" +) + +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" + 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(ingressName string, annotations map[string]string) (*UptimeCheck, error) { + id, ok := annotations[AnnotationID] + if !ok { + return nil, fmt.Errorf("%s annotation not found on ingress route: %s", AnnotationID, ingressName) + } + name, ok := annotations[AnnotationName] + if !ok { + return nil, fmt.Errorf("%s annotation not found on ingress route: %s", AnnotationName, ingressName) + } + url, ok := annotations[AnnotationURL] + if !ok { + 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], + } + if !slices.Contains(check.Tags, tagManagedBy) { + check.Tags = append(check.Tags, tagManagedBy) + } + return check, nil +} + +func kvStringToMap(s string) map[string]string { + 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 { + continue + } + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + result[key] = value + } + return result +} + +func stringToSlice(s string) []string { + if s == "" { + return nil + } + var result []string + splits := strings.Split(s, ",") + for _, part := range splits { + result = append(result, strings.TrimSpace(part)) + } + return result +} 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/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/service/provider.go b/internal/service/provider.go new file mode 100644 index 0000000..6568a7d --- /dev/null +++ b/internal/service/provider.go @@ -0,0 +1,17 @@ +package service + +import ( + "github.com/PDOK/uptime-operator/internal/model" +) + +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/providers/mock.go b/internal/service/providers/mock.go new file mode 100644 index 0000000..37b1d5a --- /dev/null +++ b/internal/service/providers/mock.go @@ -0,0 +1,41 @@ +package providers + +import ( + "encoding/json" + "log" + + "github.com/PDOK/uptime-operator/internal/model" +) + +type MockUptimeProvider struct { + checks map[string]model.UptimeCheck +} + +func NewMockUptimeProvider() *MockUptimeProvider { + 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("MOCK: 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("MOCK: deleted check %s\n", checkJSON) + + return nil +} diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 0000000..38d5ea8 --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,98 @@ +package service + +import ( + "context" + "fmt" + + m "github.com/PDOK/uptime-operator/internal/model" + "github.com/PDOK/uptime-operator/internal/service/providers" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type UptimeCheckOption func(*UptimeCheckService) *UptimeCheckService + +type UptimeCheckService struct { + provider UptimeProvider + slack *Slack +} + +func New(options ...UptimeCheckOption) *UptimeCheckService { + service := &UptimeCheckService{} + for _, option := range options { + service = option(service) + } + return service +} + +func WithProvider(provider UptimeProvider) UptimeCheckOption { + return func(service *UptimeCheckService) *UptimeCheckService { + service.provider = provider + return service + } +} + +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 + } +} + +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 + } + 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) + log.FromContext(ctx).Error(err, msg, "check", check) + if r.slack == nil { + 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 { + r.slack.Send(ctx, ":large_green_square: "+msg) + } +} diff --git a/internal/service/slack.go b/internal/service/slack.go new file mode 100644 index 0000000..e47adf1 --- /dev/null +++ b/internal/service/slack.go @@ -0,0 +1,33 @@ +package service + +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) 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(":up:"), + ) + if err != nil { + log.FromContext(ctx).Error(err, "failed to post Slack message", + "message", message, "channel", channelID, "timestamp", timestamp) + } +} 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 fb55370..a6e3b1a 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,8 +28,8 @@ 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. diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index b877b9d..13c45b8 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,8 +29,8 @@ 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" ) @@ -63,7 +71,7 @@ var _ = Describe("controller", Ordered, func() { var projectimage = "example.com/uptime-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 }