From 59920ed39c8b6073d43ebb09eb63004788e885ff Mon Sep 17 00:00:00 2001 From: "Dmitriy S. Sinyavskiy" Date: Wed, 11 Jan 2023 17:03:11 +0300 Subject: [PATCH 001/173] fixed: wrong math comparator in SLI desciption SLI Error wrong <400ms condition not matching the formula in the code of sli error_query --- examples/kubernetes-apiserver.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/kubernetes-apiserver.yml b/examples/kubernetes-apiserver.yml index f6eb91ab..fd907ba9 100644 --- a/examples/kubernetes-apiserver.yml +++ b/examples/kubernetes-apiserver.yml @@ -11,7 +11,7 @@ # # - `requests-latency` # - This SLO warn us that we apiserver responses are being slow and this will affect the clients (kubectl users, controllers...). -# - SLI error: We consider a bad request (event) when the response latency is <400ms. +# - SLI error: We consider a bad request (event) when the response latency is >400ms. # - SLO objective(99%): We have a relaxed objective because Kubernetes has a lot of async and eventual consistency flows. We could # create in a future another SLO that is less restrictive and use the latency of the realtime requests (e.g: kubectl). # From 7aca315cd2b8a2c16d0e18da7d53c8cd84b47107 Mon Sep 17 00:00:00 2001 From: QuantumEnigmaa Date: Thu, 30 Mar 2023 16:13:31 +0200 Subject: [PATCH 002/173] Split image registry and repository from Helm chart --- CHANGELOG.md | 4 ++++ deploy/kubernetes/helm/sloth/templates/deployment.yaml | 2 +- deploy/kubernetes/helm/sloth/values.yaml | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e24d724..be486247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Split image registry and repository in Helm chart + ## [v0.11.0] - 2022-10-22 ### Changed diff --git a/deploy/kubernetes/helm/sloth/templates/deployment.yaml b/deploy/kubernetes/helm/sloth/templates/deployment.yaml index f4b75491..4bfb4e20 100644 --- a/deploy/kubernetes/helm/sloth/templates/deployment.yaml +++ b/deploy/kubernetes/helm/sloth/templates/deployment.yaml @@ -29,7 +29,7 @@ spec: {{- end }} containers: - name: sloth - image: {{ .Values.image.repository }}:{{ .Values.image.tag }} + image: {{ coalesce .Values.global.imageRegistry .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }} args: - kubernetes-controller {{- if .Values.sloth.resyncInterval }} diff --git a/deploy/kubernetes/helm/sloth/values.yaml b/deploy/kubernetes/helm/sloth/values.yaml index 8c8b309d..c56b38b1 100644 --- a/deploy/kubernetes/helm/sloth/values.yaml +++ b/deploy/kubernetes/helm/sloth/values.yaml @@ -1,7 +1,11 @@ +global: + imageRegistry: "" + labels: {} image: - repository: ghcr.io/slok/sloth + registry: ghcr.io + repository: slok/sloth tag: v0.11.0 # -- Container resources: requests and limits for CPU, Memory From 5a75f85b3d4fb09260efd9c05953efb5d417cd96 Mon Sep 17 00:00:00 2001 From: Ron Ballesteros Date: Fri, 12 May 2023 22:55:07 -1000 Subject: [PATCH 003/173] fix deployment template --- deploy/kubernetes/helm/sloth/templates/deployment.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/kubernetes/helm/sloth/templates/deployment.yaml b/deploy/kubernetes/helm/sloth/templates/deployment.yaml index f4b75491..276e7fc8 100644 --- a/deploy/kubernetes/helm/sloth/templates/deployment.yaml +++ b/deploy/kubernetes/helm/sloth/templates/deployment.yaml @@ -27,6 +27,10 @@ spec: securityContext: {{- toYaml . | nindent 8 }} {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | trim | indent 8 }} + {{- end }} containers: - name: sloth image: {{ .Values.image.repository }}:{{ .Values.image.tag }} @@ -86,10 +90,6 @@ spec: {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.tolerations }} - tolerations: -{{ toYaml . | trim | indent 12 }} - {{- end }} {{- if .Values.commonPlugins.enabled }} - name: git-sync-plugins image: {{ .Values.commonPlugins.image.repository }}:{{ .Values.commonPlugins.image.tag }} From a7ca7bbdacb0f37ff84619d7eeb8c22337d17674 Mon Sep 17 00:00:00 2001 From: Ron Ballesteros Date: Fri, 12 May 2023 23:00:03 -1000 Subject: [PATCH 004/173] bump minor version --- deploy/kubernetes/helm/sloth/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/kubernetes/helm/sloth/Chart.yaml b/deploy/kubernetes/helm/sloth/Chart.yaml index b1250cfc..597651e7 100644 --- a/deploy/kubernetes/helm/sloth/Chart.yaml +++ b/deploy/kubernetes/helm/sloth/Chart.yaml @@ -4,4 +4,4 @@ description: Base chart for Sloth. type: application home: https://github.com/slok/sloth kubeVersion: ">= 1.19.0-0" -version: 0.7.0 +version: 0.7.1 From 87890099605f9e51c3b136776394c73d80da1e93 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Tue, 13 Jun 2023 11:32:02 +0800 Subject: [PATCH 005/173] fix function name in comment Signed-off-by: cui fliter --- internal/prometheus/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/prometheus/model.go b/internal/prometheus/model.go index f2ac02c1..fa3a79eb 100644 --- a/internal/prometheus/model.go +++ b/internal/prometheus/model.go @@ -227,7 +227,7 @@ func validateSLIEvents(sl validator.StructLevel) { } } -// validateOneSLIType validates only one SLI type is set and configured. +// validateOneSLI validates only one SLI type is set and configured. func validateOneSLI(sl validator.StructLevel) { sli, ok := sl.Current().Interface().(SLI) if !ok { From 7cd9a9f2e02ec995325a177132e62f79463b9107 Mon Sep 17 00:00:00 2001 From: guoguangwu Date: Fri, 8 Sep 2023 09:26:17 +0800 Subject: [PATCH 006/173] chore: pkg imported more than once Signed-off-by: guoguangwu --- internal/prometheus/sli_plugin.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/prometheus/sli_plugin.go b/internal/prometheus/sli_plugin.go index 17eb9c87..98b3a2a4 100644 --- a/internal/prometheus/sli_plugin.go +++ b/internal/prometheus/sli_plugin.go @@ -13,7 +13,6 @@ import ( "github.com/traefik/yaegi/stdlib" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/pkg/prometheus/plugin/v1" pluginv1 "github.com/slok/sloth/pkg/prometheus/plugin/v1" ) @@ -58,7 +57,7 @@ func (f fileManager) ReadFile(_ context.Context, path string) ([]byte, error) { type SLIPlugin struct { ID string - Func plugin.SLIPlugin + Func pluginv1.SLIPlugin } type FileSLIPluginRepoConfig struct { From da27684e04e4b5f646c47eab12abf6470e828af3 Mon Sep 17 00:00:00 2001 From: Kaan Karakaya Date: Thu, 29 Feb 2024 14:41:51 +0100 Subject: [PATCH 007/173] Allow spec files with CRLF Signed-off-by: Kaan Karakaya --- internal/prometheus/spec.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/prometheus/spec.go b/internal/prometheus/spec.go index 1935aa38..9db23bd2 100644 --- a/internal/prometheus/spec.go +++ b/internal/prometheus/spec.go @@ -30,7 +30,7 @@ func NewYAMLSpecLoader(pluginsRepo SLIPluginRepo, windowPeriod time.Duration) YA } } -var specTypeV1Regex = regexp.MustCompile(`(?m)^version: +['"]?prometheus\/v1['"]? *$`) +var specTypeV1Regex = regexp.MustCompile(`(?m)^version: +['"]?prometheus/v1['"]?\r?\n? *$`) func (y YAMLSpecLoader) IsSpecType(ctx context.Context, data []byte) bool { return specTypeV1Regex.Match(data) From 9ca17dad678fbdf9516f0e66f1cc144347c07d94 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 22 Mar 2025 11:09:08 +0100 Subject: [PATCH 008/173] Getting up to date with dependencies Signed-off-by: Xabier Larrakoetxea --- .github/workflows/ci.yaml | 71 +- .github/workflows/generate.yaml | 9 +- .github/workflows/helmrelease.yaml | 8 +- .golangci.yml | 9 +- CHANGELOG.md | 6 + cmd/sloth/commands/commands.go | 2 +- cmd/sloth/commands/generate.go | 2 +- cmd/sloth/commands/k8scontroller.go | 2 +- cmd/sloth/commands/validate.go | 2 +- cmd/sloth/commands/version.go | 4 +- cmd/sloth/main.go | 2 +- ...loth.slok.dev_prometheusservicelevels.yaml | 94 +- deploy/kubernetes/helm/sloth/tests/go.sum | 2 +- .../raw/sloth-with-common-plugins.yaml | 13 +- deploy/kubernetes/raw/sloth.yaml | 13 +- docker/dev/Dockerfile | 10 +- docker/prod/Dockerfile | 2 +- go.mod | 160 ++- go.sum | 1136 +++-------------- .../prometheus_rules_ensurer.go | 15 +- internal/k8sprometheus/storage.go | 8 +- internal/prometheus/model_test.go | 20 +- .../prometheus/prometheusmock/file_manager.go | 29 +- pkg/kubernetes/api/sloth/v1/README.md | 198 +-- .../gen/clientset/versioned/clientset.go | 7 +- pkg/kubernetes/gen/clientset/versioned/doc.go | 4 - .../versioned/fake/clientset_generated.go | 6 +- .../v1/fake/fake_prometheusservicelevel.go | 138 +- .../typed/sloth/v1/fake/fake_sloth_client.go | 2 +- .../typed/sloth/v1/prometheusservicelevel.go | 163 +-- .../versioned/typed/sloth/v1/sloth_client.go | 10 +- ...loth.slok.dev_prometheusservicelevels.yaml | 94 +- .../gen/informers/externalversions/factory.go | 246 ++++ .../gen/informers/externalversions/generic.go | 46 + .../internalinterfaces/factory_interfaces.go | 24 + .../externalversions/sloth/interface.go | 30 + .../externalversions/sloth/v1/interface.go | 29 + .../sloth/v1/prometheusservicelevel.go | 74 ++ .../listers/sloth/v1/expansion_generated.go | 11 + .../sloth/v1/prometheusservicelevel.go | 54 + pkg/prometheus/alertwindows/v1/README.md | 42 +- pkg/prometheus/api/v1/README.md | 46 +- scripts/kubegen.sh | 27 +- 43 files changed, 1282 insertions(+), 1588 deletions(-) delete mode 100644 pkg/kubernetes/gen/clientset/versioned/doc.go create mode 100644 pkg/kubernetes/gen/informers/externalversions/factory.go create mode 100644 pkg/kubernetes/gen/informers/externalversions/generic.go create mode 100644 pkg/kubernetes/gen/informers/externalversions/internalinterfaces/factory_interfaces.go create mode 100644 pkg/kubernetes/gen/informers/externalversions/sloth/interface.go create mode 100644 pkg/kubernetes/gen/informers/externalversions/sloth/v1/interface.go create mode 100644 pkg/kubernetes/gen/informers/externalversions/sloth/v1/prometheusservicelevel.go create mode 100644 pkg/kubernetes/gen/listers/sloth/v1/expansion_generated.go create mode 100644 pkg/kubernetes/gen/listers/sloth/v1/prometheusservicelevel.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 82aa28e3..8b762524 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,21 +7,25 @@ jobs: name: Check runs-on: ubuntu-latest # Execute the checks inside the container instead the VM. - container: golangci/golangci-lint:v1.50.0-alpine + container: golangci/golangci-lint:v1.64.8-alpine steps: - - uses: actions/checkout@v3 - - run: ./scripts/check/check.sh + - uses: actions/checkout@v4 + - run: | + # We need this go flag because it started to error after golangci-lint is using Go 1.21. + # TODO(slok): Remove it on next (>1.54.1) golangci-lint upgrade to check if this problem has gone. + export GOFLAGS="-buildvcs=false" + ./scripts/check/check.sh unit-test: - name: Unit test + name: Unit testq runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: go-version-file: go.mod - run: make ci-test - - uses: codecov/codecov-action@v3.1.1 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_UPLOAD_TOKEN }} file: ./.test_coverage.txt @@ -31,8 +35,8 @@ jobs: name: Helm chart test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Execute tests @@ -48,8 +52,8 @@ jobs: name: Integration test CLI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Execute tests @@ -68,20 +72,20 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - kubernetes: [1.21.14, 1.22.15, 1.23.12, 1.24.6, 1.25.2] + kubernetes: [1.29.14, 1.30.10, 1.31.6, 1.32.2] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Execute tests env: - KIND_VERSION: v0.16.0 + KIND_VERSION: v0.27.0 run: | # Get dependencies. echo "Getting dependencies..." curl -Lo kind https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64 && chmod +x kind && sudo mv kind /usr/local/bin/ - curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v${{ matrix.kubernetes }}/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/ + curl -Lo kubectl http://cdn.dl.k8s.io/release/v${{ matrix.kubernetes }}/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/ # Start cluster. echo "Starting ${{ matrix.kubernetes }} Kubernetes cluster..." @@ -109,11 +113,18 @@ jobs: TAG_IMAGE_LATEST: "true" PROD_IMAGE_NAME: ghcr.io/${GITHUB_REPOSITORY} VERSION: ${GITHUB_SHA} - needs: [check, unit-test, integration-test-cli, integration-test-k8s, helm-chart-test] + needs: + [ + check, + unit-test, + integration-test-cli, + integration-test-k8s, + helm-chart-test, + ] name: Release images runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Docker login run: docker login ghcr.io -u ${{ github.actor }} -p "${{ secrets.GITHUB_TOKEN }}" - name: Build and publish docker images @@ -124,12 +135,19 @@ jobs: if: startsWith(github.ref, 'refs/tags/') env: PROD_IMAGE_NAME: ghcr.io/${GITHUB_REPOSITORY} - needs: [check, unit-test, integration-test-cli, integration-test-k8s, helm-chart-test] + needs: + [ + check, + unit-test, + integration-test-cli, + integration-test-k8s, + helm-chart-test, + ] name: Tagged release images runs-on: ubuntu-latest steps: - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} # Sets VERSION env var. - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Docker login run: docker login ghcr.io -u ${{ github.actor }} -p "${{ secrets.GITHUB_TOKEN }}" - name: Build and publish docker images @@ -138,19 +156,26 @@ jobs: tagged-release-binaries: # Only on tags. if: startsWith(github.ref, 'refs/tags/') - needs: [check, unit-test, integration-test-cli, integration-test-k8s, helm-chart-test] + needs: + [ + check, + unit-test, + integration-test-cli, + integration-test-k8s, + helm-chart-test, + ] name: Tagged release binaries runs-on: ubuntu-latest steps: - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} # Sets VERSION env var. - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build binaries run: | mkdir -p ./bin chmod -R 0777 ./bin make build-all - name: Upload binaries - uses: xresloader/upload-to-github-release@v1.3.9 + uses: xresloader/upload-to-github-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/generate.yaml b/.github/workflows/generate.yaml index d4582f86..d8e4006e 100644 --- a/.github/workflows/generate.yaml +++ b/.github/workflows/generate.yaml @@ -11,16 +11,15 @@ jobs: name: Generate the SLOs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: download and setup generator binary - run: | + run: | wget https://github.com/slok/sloth/releases/download/v0.9.0/sloth-linux-amd64 chmod +x sloth-linux-amd64 ./sloth-linux-amd64 generate -i ./examples/getting-started.yml -o ./examples/_gen/getting-started.yml ./sloth-linux-amd64 generate -i ./examples/no-alerts.yml -o ./examples/_gen/no-alerts.yml - - name: 'Upload directory with generated SLOs' - uses: actions/upload-artifact@v3 + - name: "Upload directory with generated SLOs" + uses: actions/upload-artifact@v4 with: name: SLOs path: examples/_gen/ - diff --git a/.github/workflows/helmrelease.yaml b/.github/workflows/helmrelease.yaml index 6f14d301..5ebb8595 100644 --- a/.github/workflows/helmrelease.yaml +++ b/.github/workflows/helmrelease.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -22,12 +22,12 @@ jobs: git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - name: Install Helm - uses: azure/setup-helm@v3.4 + uses: azure/setup-helm@v4 with: - version: v3.7.1 + version: v3.17.0 - name: Run chart-releaser - uses: helm/chart-releaser-action@v1.4.1 + uses: helm/chart-releaser-action@v1 with: charts_dir: deploy/kubernetes/helm env: diff --git a/.golangci.yml b/.golangci.yml index 2f5cbc8a..95b937c6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -10,5 +10,12 @@ linters: - goimports - revive - gofmt - - depguard + #- depguard - godot + +linters-settings: + revive: + rules: + # Spammy linter and complex to fix on lots of parameters. Makes more harm that it solves. + - name: unused-parameter + disabled: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e24d724..41828fc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Changed + +- Update to Go 1.24. +- Update to Kubernetes v1.32. +- Update all other dependencies to latest versions. + ## [v0.11.0] - 2022-10-22 ### Changed diff --git a/cmd/sloth/commands/commands.go b/cmd/sloth/commands/commands.go index a02cda71..23449aba 100644 --- a/cmd/sloth/commands/commands.go +++ b/cmd/sloth/commands/commands.go @@ -4,7 +4,7 @@ import ( "context" "io" - "gopkg.in/alecthomas/kingpin.v2" + "github.com/alecthomas/kingpin/v2" "github.com/slok/sloth/internal/log" ) diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 2d5c24ae..2b2f85bf 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -13,8 +13,8 @@ import ( "time" openslov1alpha "github.com/OpenSLO/oslo/pkg/manifest/v1alpha" + "github.com/alecthomas/kingpin/v2" prometheusmodel "github.com/prometheus/common/model" - "gopkg.in/alecthomas/kingpin.v2" "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/app/generate" diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index 5b0ffbff..0b554d34 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -12,6 +12,7 @@ import ( "syscall" "time" + "github.com/alecthomas/kingpin/v2" "github.com/oklog/run" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" monitoringclientset "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" @@ -21,7 +22,6 @@ import ( koopercontroller "github.com/spotahome/kooper/v2/controller" kooperlog "github.com/spotahome/kooper/v2/log" kooperprometheus "github.com/spotahome/kooper/v2/metrics/prometheus" - "gopkg.in/alecthomas/kingpin.v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/watch" diff --git a/cmd/sloth/commands/validate.go b/cmd/sloth/commands/validate.go index 1dc68cf3..efc5f8e0 100644 --- a/cmd/sloth/commands/validate.go +++ b/cmd/sloth/commands/validate.go @@ -9,8 +9,8 @@ import ( "regexp" "time" + "github.com/alecthomas/kingpin/v2" prometheusmodel "github.com/prometheus/common/model" - "gopkg.in/alecthomas/kingpin.v2" "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/k8sprometheus" diff --git a/cmd/sloth/commands/version.go b/cmd/sloth/commands/version.go index 1813c581..d875a088 100644 --- a/cmd/sloth/commands/version.go +++ b/cmd/sloth/commands/version.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "gopkg.in/alecthomas/kingpin.v2" + "github.com/alecthomas/kingpin/v2" "github.com/slok/sloth/internal/info" ) @@ -21,6 +21,6 @@ func NewVersionCommand(app *kingpin.Application) Command { func (versionCommand) Name() string { return "version" } func (versionCommand) Run(ctx context.Context, config RootConfig) error { - fmt.Fprintf(config.Stdout, info.Version) + fmt.Fprint(config.Stdout, info.Version) return nil } diff --git a/cmd/sloth/main.go b/cmd/sloth/main.go index 55f044bd..bc397d33 100644 --- a/cmd/sloth/main.go +++ b/cmd/sloth/main.go @@ -6,8 +6,8 @@ import ( "io" "os" + "github.com/alecthomas/kingpin/v2" "github.com/sirupsen/logrus" - "gopkg.in/alecthomas/kingpin.v2" "github.com/slok/sloth/cmd/sloth/commands" "github.com/slok/sloth/internal/info" diff --git a/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml b/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml index 7283e667..83cd4c98 100644 --- a/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml +++ b/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml @@ -4,7 +4,6 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: (devel) - creationTimestamp: null name: prometheusservicelevels.sloth.slok.dev spec: group: sloth.slok.dev @@ -45,18 +44,24 @@ spec: name: v1 schema: openAPIV3Schema: - description: PrometheusServiceLevel is the expected service quality level - using Prometheus as the backend used by Sloth. + description: |- + PrometheusServiceLevel is the expected service quality level using Prometheus + as the backend used by Sloth. properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -66,8 +71,9 @@ spec: labels: additionalProperties: type: string - description: Labels are the Prometheus labels that will have all the - recording and alerting rules generated for the service SLOs. + description: |- + Labels are the Prometheus labels that will have all the recording + and alerting rules generated for the service SLOs. type: object service: description: Service is the application of the SLOs. @@ -75,18 +81,21 @@ spec: slos: description: SLOs are the SLOs of the service. items: - description: SLO is the configuration/declaration of the service - level objective of a service. + description: |- + SLO is the configuration/declaration of the service level objective of + a service. properties: alerting: - description: Alerting is the configuration with all the things - related with the SLO alerts. + description: |- + Alerting is the configuration with all the things related with the SLO + alerts. properties: annotations: additionalProperties: type: string - description: Annotations are the Prometheus annotations - that will have all the alerts generated by this SLO. + description: |- + Annotations are the Prometheus annotations that will have all the alerts generated by + this SLO. type: object labels: additionalProperties: @@ -109,16 +118,16 @@ spec: for the specific alert. type: object disable: - description: Disable disables the alert and makes Sloth - not generating this alert. This can be helpful for - example to disable ticket(warning) alerts. + description: |- + Disable disables the alert and makes Sloth not generating this alert. This + can be helpful for example to disable ticket(warning) alerts. type: boolean labels: additionalProperties: type: string - description: Labels are the Prometheus labels for the - specific alert. For example can be useful to route - the Page alert to specific Slack channel. + description: |- + Labels are the Prometheus labels for the specific alert. For example can be + useful to route the Page alert to specific Slack channel. type: object type: object ticketAlert: @@ -132,16 +141,16 @@ spec: for the specific alert. type: object disable: - description: Disable disables the alert and makes Sloth - not generating this alert. This can be helpful for - example to disable ticket(warning) alerts. + description: |- + Disable disables the alert and makes Sloth not generating this alert. This + can be helpful for example to disable ticket(warning) alerts. type: boolean labels: additionalProperties: type: string - description: Labels are the Prometheus labels for the - specific alert. For example can be useful to route - the Page alert to specific Slack channel. + description: |- + Labels are the Prometheus labels for the specific alert. For example can be + useful to route the Page alert to specific Slack channel. type: object type: object type: object @@ -151,9 +160,10 @@ spec: labels: additionalProperties: type: string - description: Labels are the Prometheus labels that will have - all the recording and alerting rules for this specific SLO. - These labels are merged with the previous level labels. + description: |- + Labels are the Prometheus labels that will have all the recording and + alerting rules for this specific SLO. These labels are merged with the + previous level labels. type: object name: description: Name is the name of the SLO. @@ -171,16 +181,16 @@ spec: description: Events is the events SLI type. properties: errorQuery: - description: ErrorQuery is a Prometheus query that will - get the number/count of events that we consider that - are bad for the SLO (e.g "http 5xx", "latency > 250ms"...). + description: |- + ErrorQuery is a Prometheus query that will get the number/count of events + that we consider that are bad for the SLO (e.g "http 5xx", "latency > 250ms"...). Requires the usage of `{{.window}}` template variable. type: string totalQuery: - description: TotalQuery is a Prometheus query that will - get the total number/count of events for the SLO (e.g - "all http requests"...). Requires the usage of `{{.window}}` - template variable. + description: |- + TotalQuery is a Prometheus query that will get the total number/count of events + for the SLO (e.g "all http requests"...). + Requires the usage of `{{.window}}` template variable. type: string required: - errorQuery @@ -231,9 +241,9 @@ spec: format: date-time type: string observedGeneration: - description: ObservedGeneration tells the generation was acted on, - normally this is required to stop an infinite loop when the status - is updated because it sends a watch updated event to the watchers + description: |- + ObservedGeneration tells the generation was acted on, normally this is required to stop an + infinite loop when the status is updated because it sends a watch updated event to the watchers of the K8s object. format: int64 type: integer diff --git a/deploy/kubernetes/helm/sloth/tests/go.sum b/deploy/kubernetes/helm/sloth/tests/go.sum index addc1880..1933aab8 100644 --- a/deploy/kubernetes/helm/sloth/tests/go.sum +++ b/deploy/kubernetes/helm/sloth/tests/go.sum @@ -944,7 +944,7 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +github.com/alecthomas/kingpin/v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml index 21de1723..faf910bd 100644 --- a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml +++ b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -89,6 +89,7 @@ spec: args: - kubernetes-controller - --sli-plugins-path=/plugins + - --logger=default ports: - containerPort: 8081 name: metrics @@ -132,7 +133,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth diff --git a/deploy/kubernetes/raw/sloth.yaml b/deploy/kubernetes/raw/sloth.yaml index 458928f3..edca04bc 100644 --- a/deploy/kubernetes/raw/sloth.yaml +++ b/deploy/kubernetes/raw/sloth.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -88,6 +88,7 @@ spec: image: ghcr.io/slok/sloth:v0.11.0 args: - kubernetes-controller + - --logger=default ports: - containerPort: 8081 name: metrics @@ -107,7 +108,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.6.4 + helm.sh/chart: sloth-0.7.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index d5a9cc4e..a1189c91 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -1,11 +1,11 @@ -FROM golang:1.19.3 +FROM golang:1.24 LABEL org.opencontainers.image.source https://github.com/slok/sloth -ARG GOLANGCI_LINT_VERSION="1.50.0" -ARG MOCKERY_VERSION="2.14.0" -ARG GOMARKDOC_VERSION="0.4.1" -ARG HELM_VERSION="3.10.0" +ARG GOLANGCI_LINT_VERSION="1.64.8" +ARG MOCKERY_VERSION="2.53.3" +ARG GOMARKDOC_VERSION="1.1.0" +ARG HELM_VERSION="3.17.0" ARG ostype=Linux RUN apt-get update && apt-get install -y \ diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index dacf5ac0..430a4cf8 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,7 +1,7 @@ # Set also `ARCH` ARG here so we can use it on all the `FROM`s. ARG ARCH -FROM golang:1.19.3-alpine as build-stage +FROM golang:1.24.0-alpine as build-stage LABEL org.opencontainers.image.source https://github.com/slok/sloth diff --git a/go.mod b/go.mod index 3bf52b60..2e55c1ac 100644 --- a/go.mod +++ b/go.mod @@ -1,108 +1,98 @@ module github.com/slok/sloth -go 1.19 +go 1.24 + +toolchain go1.24.0 require ( - github.com/OpenSLO/oslo v0.2.2-0.20210629193748-b882029ce777 - github.com/go-playground/validator/v10 v10.11.1 + github.com/OpenSLO/oslo v0.12.0 + github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/go-playground/validator/v10 v10.25.0 github.com/oklog/run v1.1.0 - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.61.1 - github.com/prometheus-operator/prometheus-operator/pkg/client v0.61.1 - github.com/prometheus/client_golang v1.14.0 - github.com/prometheus/common v0.37.0 - github.com/prometheus/prometheus v0.40.3 - github.com/sirupsen/logrus v1.9.0 - github.com/slok/reload v0.1.0 - github.com/spotahome/kooper/v2 v2.2.0 - github.com/stretchr/testify v1.8.1 - github.com/traefik/yaegi v0.14.3 - gopkg.in/alecthomas/kingpin.v2 v2.2.6 + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.81.0 + github.com/prometheus-operator/prometheus-operator/pkg/client v0.81.0 + github.com/prometheus/client_golang v1.21.1 + github.com/prometheus/common v0.63.0 + github.com/prometheus/prometheus v0.302.2-0.20250320225832-3d603d19575e + github.com/sirupsen/logrus v1.9.3 + github.com/slok/reload v0.2.0 + github.com/spotahome/kooper/v2 v2.8.0 + github.com/stretchr/testify v1.10.0 + github.com/traefik/yaegi v0.16.1 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.25.4 - k8s.io/apimachinery v0.25.4 - k8s.io/client-go v0.25.4 + k8s.io/api v0.32.3 + k8s.io/apimachinery v0.32.3 + k8s.io/client-go v0.32.3 ) require ( - cloud.google.com/go v0.97.0 // indirect - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.28 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect - github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect - github.com/aws/aws-sdk-go v1.44.128 // indirect + github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect - github.com/edsrzf/mmap-go v1.1.0 // indirect - github.com/emicklei/go-restful/v3 v3.10.0 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/go-kit/log v0.2.1 // indirect - github.com/go-logfmt/logfmt v0.5.1 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/edsrzf/mmap-go v1.2.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect - github.com/go-openapi/swag v0.22.3 // indirect - github.com/go-playground/locales v0.14.0 // indirect - github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.2.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/gnostic v0.6.9 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/grafana/regexp v0.0.0-20221005093135-b4c2bcb0a4b6 // indirect - github.com/imdario/mergo v0.3.12 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/leodido/go-urn v1.2.1 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.9.0 // 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/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect - github.com/oklog/ulid v1.3.1 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common/sigv4 v0.1.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.5.0 // indirect - go.opentelemetry.io/otel v1.11.1 // indirect - go.opentelemetry.io/otel/trace v1.11.1 // indirect - go.uber.org/atomic v1.10.0 // indirect - go.uber.org/goleak v1.2.0 // indirect - golang.org/x/crypto v0.1.0 // indirect - golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect - golang.org/x/net v0.2.0 // indirect - golang.org/x/oauth2 v0.2.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.2.0 // indirect - golang.org/x/term v0.2.0 // indirect - golang.org/x/text v0.4.0 // indirect - golang.org/x/time v0.2.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/procfs v0.16.0 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.31.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.25.4 // indirect - k8s.io/klog/v2 v2.80.1 // indirect - k8s.io/kube-openapi v0.0.0-20221110221610-a28e98eb7c70 // indirect - k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2 // indirect - sigs.k8s.io/controller-runtime v0.13.1 // indirect - sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + k8s.io/apiextensions-apiserver v0.32.3 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect + sigs.k8s.io/controller-runtime v0.20.3 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 75a16d6d..3b4b954a 100644 --- a/go.sum +++ b/go.sum @@ -1,1015 +1,295 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0 h1:3DXvAyifywvq64LfkKaMOmkWPS1CikIQdMe2lY9vxU8= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go v65.0.0+incompatible h1:HzKLt3kIwMm4KeJYTdx9EbjRYTySD/t8i1Ee/W5EGXw= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM= -github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= -github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/adal v0.9.21 h1:jjQnVFXPfekaqb8vIsv2G1lxshoW+oGv4MDlhRtnYZk= -github.com/Azure/go-autorest/autorest/adal v0.9.21/go.mod h1:zua7mBUaCc5YnSLKYgGJR/w5ePdMDA6H56upLsHzA9U= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= -github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= -github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= -github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/OpenSLO/oslo v0.2.2-0.20210629193748-b882029ce777 h1:Qoh0NZ1TnWjxP6P3xh2w6IqDn/m85NzTSyRZ8UMXra0= -github.com/OpenSLO/oslo v0.2.2-0.20210629193748-b882029ce777/go.mod h1:oNu7jsjtXU8ct/VR0znkvBpNvlOSuc5sN/z82Vrkycs= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= -github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-metrics v0.3.10 h1:FR+drcQStOe+32sYyJYyZ7FIdgoGGBnwLl+flodp8Uo= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= -github.com/aws/aws-sdk-go v1.44.128 h1:X34pX5t0LIZXjBY11yf9JKMP3c1aZgirh+5PjtaZyJ4= -github.com/aws/aws-sdk-go v1.44.128/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/OpenSLO/oslo v0.12.0 h1:0zdxMgFE59TUKpe/L4+5ujgmJZW/kAuCOJbyqrTX4lc= +github.com/OpenSLO/oslo v0.12.0/go.mod h1:6jyOTkqBCdkgqLXJ6WtJj7+o0rSexl6YZbWwnxpGwtU= +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc h1:PYXxkRUBGUMa5xgMVMDl62vEklZvKpVaxQeN9ie7Hfk= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/digitalocean/godo v1.88.0 h1:SAEdw63xOMmzlwCeCWjLH1GcyDPUjbSAR1Bh7VELxzc= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= -github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= -github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= -github.com/emicklei/go-restful/v3 v3.10.0 h1:X4gma4HM7hFm6WMeAsTfqA0GOfdNoCzBIkHGoRLGXuM= -github.com/emicklei/go-restful/v3 v3.10.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.10.3 h1:xdCVXxEe0Y3FQith+0cj2irwZudqGYvecuLB1HtdexY= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.13 h1:TvDcILLkjuZV3ER58VkBmncKsLUBqBDxra/XctCzuMM= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= +github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= +github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator/v10 v10.6.1/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= -github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= -github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= -github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 h1:JVrqSeQfdhYRFk24TvhTZWU0q8lfCojxZQFi3Ou7+uY= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= +github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= -github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/gophercloud/gophercloud v1.0.0 h1:9nTGx0jizmHxDobe4mck89FyQHVyA3CaXLIUSGJjP9k= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/grafana/regexp v0.0.0-20221005093135-b4c2bcb0a4b6 h1:A3dhViTeFDSQcGOXuUi6ukCQSMyDtDISBp2z6OOo2YM= -github.com/grafana/regexp v0.0.0-20221005093135-b4c2bcb0a4b6/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.15.3 h1:WYONYL2rxTXtlekAqblR2SCdJsizMDIj/uXb5wNy9zU= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/cronexpr v1.1.1 h1:NJZDd87hGXjoZBdvyCF9mX4DCq5Wy7+A/w+A7q0wn6c= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrhxx+FVELeXpVPE= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/nomad/api v0.0.0-20221102143410-8a95f1239005 h1:jKwXhVS4F7qk0g8laz+Anz0g/6yaSJ3HqmSAuSNLUcA= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/serf v0.9.7 h1:hkdgbqizGQHuU5IPqYM1JdSMV8nKfpuOnZYXssk9muY= -github.com/hetznercloud/hcloud-go v1.35.3 h1:WCmFAhLRooih2QHAsbCbEdpIHnshQQmrPqsr3rHE1Ow= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/ionos-cloud/sdk-go/v6 v6.1.3 h1:vb6yqdpiqaytvreM0bsn2pXw+1YDvEk2RKSmBAQvgDQ= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.5 h1:VgzTY2jogw3xt39CusEnFJWm7rlsq5yL5q9XdLOuP5g= +github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 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/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/linode/linodego v1.9.3 h1:+lxNZw4avRxhCqGjwfPgQ2PvMT+vOL0OMsTdzixR7hQ= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 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= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU= -github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.61.1 h1:ViIkBYnAUumtx9D7PiVPc1n8kNvwm+WMepDZWTZCBPc= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.61.1/go.mod h1:j51242bf6LQwvJ1JPKWApzTnifmCwcQq0i1p29ylWiM= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.61.1 h1:y5ILBCB26Jztm/lgPwm7EcIPxfG20NbY8irIvCIZfKg= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.61.1/go.mod h1:hnvR2Lm/j9sLB1mZHl9gwnuzHuC3iyX4eUPx1SVogF8= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= -github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/prometheus/prometheus v0.40.3 h1:oMw1vVyrxHTigXAcFY6QHrGUnQEbKEOKo737cPgYBwY= -github.com/prometheus/prometheus v0.40.3/go.mod h1:/UhsWkOXkO11wqTW2Bx5YDOwRweSDcaFBlTIzFe7P0Y= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.9 h1:0roa6gXKgyta64uqh52AQG3wzZXH21unn+ltzQSXML0= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/slok/reload v0.1.0 h1:VEkUHiV+7WCJ5+zKxuWhD41NFpr1G7ACILz43aXuP+8= -github.com/slok/reload v0.1.0/go.mod h1:rQBFU7T77Rrm9zMyZRtCWexRBcFesEGPWj72KPzra/A= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -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/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spotahome/kooper/v2 v2.2.0 h1:pzE7Gcqwwd75uJpDDmyJSkSbkPTggb6VF/vljgu8Vx0= -github.com/spotahome/kooper/v2 v2.2.0/go.mod h1:ul2uK/vPBRCifCJUsxQL3+ccgj8gPETFMVtAmCRx42U= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +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-operator/prometheus-operator/pkg/apis/monitoring v0.81.0 h1:mSii7z+TihzdeULnGjLnNikgtDbeViY/wW8s3430rhE= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.81.0/go.mod h1:YfnEQzw7tUQa0Sjiz8V6QFc6JUGE+i5wybsjc3EOKn8= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.81.0 h1:z8ETgiD2hThJi3+3S8eKAbC9/pwPq1kGt8HkeGlDstw= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.81.0/go.mod h1:EKK9z5OIxxwaCZmgaidiIfvy6mF7x8M3AJHmxwx+QB4= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= +github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= +github.com/prometheus/prometheus v0.302.2-0.20250320225832-3d603d19575e h1:04tg0Q3/UCwa5ATaa+a1s8aJasOuoPfeyL7FzNDc4rM= +github.com/prometheus/prometheus v0.302.2-0.20250320225832-3d603d19575e/go.mod h1:6Wxk+7sG0er66SyZFfcNTAflpes8MrRuBoZni+abkrI= +github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8= +github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAoaohfjqMU= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slok/reload v0.2.0 h1:8ezO7EsaYMUCX2g1ZAdHTxD0Mo9oDn2legZZBoFMUEI= +github.com/slok/reload v0.2.0/go.mod h1:vyJiGsSZTUx08vDoEiVszBp8Jr+bsMwIK/U1pK7XTmI= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spotahome/kooper/v2 v2.8.0 h1:qAbT1kF5vC39jxr74dNqiBT9ZxxijBNqOsuZoJmgE88= +github.com/spotahome/kooper/v2 v2.8.0/go.mod h1:lBcjQOn/pbRS4F8jXZ5IRH7pcE3vhdXwz7cD9HtHiRM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -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 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/traefik/yaegi v0.14.3 h1:LqA0k8DKwvRMc+msfQjNusphHJc+r6WC5tZU5TmUFOM= -github.com/traefik/yaegi v0.14.3/go.mod h1:AVRxhaI2G+nUsaM1zyktzwXn69G3t/AuTDrCiTds9p0= -github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E= +github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4= -go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE= -go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ= -go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= -golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= 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.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU= -golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= -golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 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= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/api v0.224.0 h1:Ir4UPtDsNiwIOHdExr3fAj4xZ42QjK7uQte3lORLJwU= +google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/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.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.25.4 h1:3YO8J4RtmG7elEgaWMb4HgmpS2CfY1QlaOz9nwB+ZSs= -k8s.io/api v0.25.4/go.mod h1:IG2+RzyPQLllQxnhzD8KQNEu4c4YvyDTpSMztf4A0OQ= -k8s.io/apiextensions-apiserver v0.25.4 h1:7hu9pF+xikxQuQZ7/30z/qxIPZc2J1lFElPtr7f+B6U= -k8s.io/apiextensions-apiserver v0.25.4/go.mod h1:bkSGki5YBoZWdn5pWtNIdGvDrrsRWlmnvl9a+tAw5vQ= -k8s.io/apimachinery v0.25.4 h1:CtXsuaitMESSu339tfhVXhQrPET+EiWnIY1rcurKnAc= -k8s.io/apimachinery v0.25.4/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo= -k8s.io/client-go v0.25.4 h1:3RNRDffAkNU56M/a7gUfXaEzdhZlYhoW8dgViGy5fn8= -k8s.io/client-go v0.25.4/go.mod h1:8trHCAC83XKY0wsBIpbirZU4NTUpbuhc2JnI7OruGZw= -k8s.io/klog/v2 v2.80.1 h1:atnLQ121W371wYYFawwYx1aEY2eUfs4l3J72wtgAwV4= -k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20221110221610-a28e98eb7c70 h1:zfqQc1V6/ZgGpvrOVvr62OjiqQX4lZjfznK34NQwkqw= -k8s.io/kube-openapi v0.0.0-20221110221610-a28e98eb7c70/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= -k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2 h1:GfD9OzL11kvZN5iArC6oTS7RTj7oJOIfnislxYlqTj8= -k8s.io/utils v0.0.0-20221108210102-8e77b1f39fe2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/controller-runtime v0.13.1 h1:tUsRCSJVM1QQOOeViGeX3GMT3dQF1eePPw6sEE3xSlg= -sigs.k8s.io/controller-runtime v0.13.1/go.mod h1:Zbz+el8Yg31jubvAEyglRZGdLAjplZl+PgtYNI6WNTI= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= +k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= +k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= +k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.3 h1:I6Ln8JfQjHH7JbtCD2HCYHoIzajoRxPNuvhvcDbZgkI= +sigs.k8s.io/controller-runtime v0.20.3/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/k8sprometheus/k8sprometheusmock/prometheus_rules_ensurer.go b/internal/k8sprometheus/k8sprometheusmock/prometheus_rules_ensurer.go index f8995be0..4549b925 100644 --- a/internal/k8sprometheus/k8sprometheusmock/prometheus_rules_ensurer.go +++ b/internal/k8sprometheus/k8sprometheusmock/prometheus_rules_ensurer.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.53.3. DO NOT EDIT. package k8sprometheusmock @@ -19,6 +19,10 @@ type PrometheusRulesEnsurer struct { func (_m *PrometheusRulesEnsurer) EnsurePrometheusRule(ctx context.Context, pr *v1.PrometheusRule) error { ret := _m.Called(ctx, pr) + if len(ret) == 0 { + panic("no return value specified for EnsurePrometheusRule") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *v1.PrometheusRule) error); ok { r0 = rf(ctx, pr) @@ -29,13 +33,12 @@ func (_m *PrometheusRulesEnsurer) EnsurePrometheusRule(ctx context.Context, pr * return r0 } -type mockConstructorTestingTNewPrometheusRulesEnsurer interface { +// NewPrometheusRulesEnsurer creates a new instance of PrometheusRulesEnsurer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPrometheusRulesEnsurer(t interface { mock.TestingT Cleanup(func()) -} - -// NewPrometheusRulesEnsurer creates a new instance of PrometheusRulesEnsurer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewPrometheusRulesEnsurer(t mockConstructorTestingTNewPrometheusRulesEnsurer) *PrometheusRulesEnsurer { +}) *PrometheusRulesEnsurer { mock := &PrometheusRulesEnsurer{} mock.Mock.Test(t) diff --git a/internal/k8sprometheus/storage.go b/internal/k8sprometheus/storage.go index 1f31f833..8527d514 100644 --- a/internal/k8sprometheus/storage.go +++ b/internal/k8sprometheus/storage.go @@ -134,11 +134,17 @@ func promRulesToKubeRules(rules []rulefmt.Rule) []monitoringv1.Rule { forS = r.For.String() } + var dur *monitoringv1.Duration + if forS != "" { + d := monitoringv1.Duration(forS) + dur = &d + } + res = append(res, monitoringv1.Rule{ Record: r.Record, Alert: r.Alert, Expr: intstr.FromString(r.Expr), - For: monitoringv1.Duration(forS), + For: dur, Labels: r.Labels, Annotations: r.Annotations, }) diff --git a/internal/prometheus/model_test.go b/internal/prometheus/model_test.go index 70d9298c..8d53fd22 100644 --- a/internal/prometheus/model_test.go +++ b/internal/prometheus/model_test.go @@ -292,10 +292,10 @@ func TestModelValidationSpec(t *testing.T) { "SLO Labels should be valid prometheus keys.": { slo: func() prometheus.SLOGroup { s := getGoodSLOGroup() - s.SLOs[0].Labels[".something"] = "label key is wrong" + s.SLOs[0].Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Labels[.something]' Error:Field validation for 'Labels[.something]' failed on the 'prom_label_key' tag", + expErrMessage: "Key: 'SLOGroup.SLOs[0].Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", }, "SLO Labels should have prometheus values.": { @@ -359,10 +359,10 @@ func TestModelValidationSpec(t *testing.T) { "SLO page alert labels should be valid prometheus keys.": { slo: func() prometheus.SLOGroup { s := getGoodSLOGroup() - s.SLOs[0].PageAlertMeta.Labels[".something"] = "label key is wrong" + s.SLOs[0].PageAlertMeta.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].PageAlertMeta.Labels[.something]' Error:Field validation for 'Labels[.something]' failed on the 'prom_label_key' tag", + expErrMessage: "Key: 'SLOGroup.SLOs[0].PageAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", }, "SLO page alert labels should have prometheus values.": { @@ -386,10 +386,10 @@ func TestModelValidationSpec(t *testing.T) { "SLO page alert annotations should be valid prometheus keys.": { slo: func() prometheus.SLOGroup { s := getGoodSLOGroup() - s.SLOs[0].PageAlertMeta.Annotations[".something"] = "label key is wrong" + s.SLOs[0].PageAlertMeta.Annotations["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].PageAlertMeta.Annotations[.something]' Error:Field validation for 'Annotations[.something]' failed on the 'prom_annot_key' tag", + expErrMessage: "Key: 'SLOGroup.SLOs[0].PageAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", }, "SLO page alert annotations should have prometheus values.": { @@ -404,10 +404,10 @@ func TestModelValidationSpec(t *testing.T) { "SLO warning alert labels should be valid prometheus keys.": { slo: func() prometheus.SLOGroup { s := getGoodSLOGroup() - s.SLOs[0].TicketAlertMeta.Labels[".something"] = "label key is wrong" + s.SLOs[0].TicketAlertMeta.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].TicketAlertMeta.Labels[.something]' Error:Field validation for 'Labels[.something]' failed on the 'prom_label_key' tag", + expErrMessage: "Key: 'SLOGroup.SLOs[0].TicketAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", }, "SLO warning alert labels should have prometheus values.": { @@ -431,10 +431,10 @@ func TestModelValidationSpec(t *testing.T) { "SLO warning alert annotations should be valid prometheus keys.": { slo: func() prometheus.SLOGroup { s := getGoodSLOGroup() - s.SLOs[0].TicketAlertMeta.Annotations[".something"] = "label key is wrong" + s.SLOs[0].TicketAlertMeta.Annotations["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].TicketAlertMeta.Annotations[.something]' Error:Field validation for 'Annotations[.something]' failed on the 'prom_annot_key' tag", + expErrMessage: "Key: 'SLOGroup.SLOs[0].TicketAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", }, "SLO warning alert annotations should have prometheus values.": { diff --git a/internal/prometheus/prometheusmock/file_manager.go b/internal/prometheus/prometheusmock/file_manager.go index 703008ed..2b0f2d2a 100644 --- a/internal/prometheus/prometheusmock/file_manager.go +++ b/internal/prometheus/prometheusmock/file_manager.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.53.3. DO NOT EDIT. package prometheusmock @@ -19,7 +19,15 @@ type FileManager struct { func (_m *FileManager) FindFiles(ctx context.Context, root string, matcher *regexp.Regexp) ([]string, error) { ret := _m.Called(ctx, root, matcher) + if len(ret) == 0 { + panic("no return value specified for FindFiles") + } + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *regexp.Regexp) ([]string, error)); ok { + return rf(ctx, root, matcher) + } if rf, ok := ret.Get(0).(func(context.Context, string, *regexp.Regexp) []string); ok { r0 = rf(ctx, root, matcher) } else { @@ -28,7 +36,6 @@ func (_m *FileManager) FindFiles(ctx context.Context, root string, matcher *rege } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, string, *regexp.Regexp) error); ok { r1 = rf(ctx, root, matcher) } else { @@ -42,7 +49,15 @@ func (_m *FileManager) FindFiles(ctx context.Context, root string, matcher *rege func (_m *FileManager) ReadFile(ctx context.Context, path string) ([]byte, error) { ret := _m.Called(ctx, path) + if len(ret) == 0 { + panic("no return value specified for ReadFile") + } + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]byte, error)); ok { + return rf(ctx, path) + } if rf, ok := ret.Get(0).(func(context.Context, string) []byte); ok { r0 = rf(ctx, path) } else { @@ -51,7 +66,6 @@ func (_m *FileManager) ReadFile(ctx context.Context, path string) ([]byte, error } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { r1 = rf(ctx, path) } else { @@ -61,13 +75,12 @@ func (_m *FileManager) ReadFile(ctx context.Context, path string) ([]byte, error return r0, r1 } -type mockConstructorTestingTNewFileManager interface { +// NewFileManager creates a new instance of FileManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFileManager(t interface { mock.TestingT Cleanup(func()) -} - -// NewFileManager creates a new instance of FileManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewFileManager(t mockConstructorTestingTNewFileManager) *FileManager { +}) *FileManager { mock := &FileManager{} mock.Mock.Test(t) diff --git a/pkg/kubernetes/api/sloth/v1/README.md b/pkg/kubernetes/api/sloth/v1/README.md index 46ef875c..1a509497 100755 --- a/pkg/kubernetes/api/sloth/v1/README.md +++ b/pkg/kubernetes/api/sloth/v1/README.md @@ -9,48 +9,50 @@ import "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" ## Index - [Variables](<#variables>) -- [func Kind(kind string) schema.GroupKind](<#func-kind>) -- [func Resource(resource string) schema.GroupResource](<#func-resource>) -- [func VersionKind(kind string) schema.GroupVersionKind](<#func-versionkind>) -- [type Alert](<#type-alert>) - - [func (in *Alert) DeepCopy() *Alert](<#func-alert-deepcopy>) - - [func (in *Alert) DeepCopyInto(out *Alert)](<#func-alert-deepcopyinto>) -- [type Alerting](<#type-alerting>) - - [func (in *Alerting) DeepCopy() *Alerting](<#func-alerting-deepcopy>) - - [func (in *Alerting) DeepCopyInto(out *Alerting)](<#func-alerting-deepcopyinto>) -- [type PrometheusServiceLevel](<#type-prometheusservicelevel>) - - [func (in *PrometheusServiceLevel) DeepCopy() *PrometheusServiceLevel](<#func-prometheusservicelevel-deepcopy>) - - [func (in *PrometheusServiceLevel) DeepCopyInto(out *PrometheusServiceLevel)](<#func-prometheusservicelevel-deepcopyinto>) - - [func (in *PrometheusServiceLevel) DeepCopyObject() runtime.Object](<#func-prometheusservicelevel-deepcopyobject>) -- [type PrometheusServiceLevelList](<#type-prometheusservicelevellist>) - - [func (in *PrometheusServiceLevelList) DeepCopy() *PrometheusServiceLevelList](<#func-prometheusservicelevellist-deepcopy>) - - [func (in *PrometheusServiceLevelList) DeepCopyInto(out *PrometheusServiceLevelList)](<#func-prometheusservicelevellist-deepcopyinto>) - - [func (in *PrometheusServiceLevelList) DeepCopyObject() runtime.Object](<#func-prometheusservicelevellist-deepcopyobject>) -- [type PrometheusServiceLevelSpec](<#type-prometheusservicelevelspec>) - - [func (in *PrometheusServiceLevelSpec) DeepCopy() *PrometheusServiceLevelSpec](<#func-prometheusservicelevelspec-deepcopy>) - - [func (in *PrometheusServiceLevelSpec) DeepCopyInto(out *PrometheusServiceLevelSpec)](<#func-prometheusservicelevelspec-deepcopyinto>) -- [type PrometheusServiceLevelStatus](<#type-prometheusservicelevelstatus>) - - [func (in *PrometheusServiceLevelStatus) DeepCopy() *PrometheusServiceLevelStatus](<#func-prometheusservicelevelstatus-deepcopy>) - - [func (in *PrometheusServiceLevelStatus) DeepCopyInto(out *PrometheusServiceLevelStatus)](<#func-prometheusservicelevelstatus-deepcopyinto>) -- [type SLI](<#type-sli>) - - [func (in *SLI) DeepCopy() *SLI](<#func-sli-deepcopy>) - - [func (in *SLI) DeepCopyInto(out *SLI)](<#func-sli-deepcopyinto>) -- [type SLIEvents](<#type-slievents>) - - [func (in *SLIEvents) DeepCopy() *SLIEvents](<#func-slievents-deepcopy>) - - [func (in *SLIEvents) DeepCopyInto(out *SLIEvents)](<#func-slievents-deepcopyinto>) -- [type SLIPlugin](<#type-sliplugin>) - - [func (in *SLIPlugin) DeepCopy() *SLIPlugin](<#func-sliplugin-deepcopy>) - - [func (in *SLIPlugin) DeepCopyInto(out *SLIPlugin)](<#func-sliplugin-deepcopyinto>) -- [type SLIRaw](<#type-sliraw>) - - [func (in *SLIRaw) DeepCopy() *SLIRaw](<#func-sliraw-deepcopy>) - - [func (in *SLIRaw) DeepCopyInto(out *SLIRaw)](<#func-sliraw-deepcopyinto>) -- [type SLO](<#type-slo>) - - [func (in *SLO) DeepCopy() *SLO](<#func-slo-deepcopy>) - - [func (in *SLO) DeepCopyInto(out *SLO)](<#func-slo-deepcopyinto>) +- [func Kind\(kind string\) schema.GroupKind](<#Kind>) +- [func Resource\(resource string\) schema.GroupResource](<#Resource>) +- [func VersionKind\(kind string\) schema.GroupVersionKind](<#VersionKind>) +- [type Alert](<#Alert>) + - [func \(in \*Alert\) DeepCopy\(\) \*Alert](<#Alert.DeepCopy>) + - [func \(in \*Alert\) DeepCopyInto\(out \*Alert\)](<#Alert.DeepCopyInto>) +- [type Alerting](<#Alerting>) + - [func \(in \*Alerting\) DeepCopy\(\) \*Alerting](<#Alerting.DeepCopy>) + - [func \(in \*Alerting\) DeepCopyInto\(out \*Alerting\)](<#Alerting.DeepCopyInto>) +- [type PrometheusServiceLevel](<#PrometheusServiceLevel>) + - [func \(in \*PrometheusServiceLevel\) DeepCopy\(\) \*PrometheusServiceLevel](<#PrometheusServiceLevel.DeepCopy>) + - [func \(in \*PrometheusServiceLevel\) DeepCopyInto\(out \*PrometheusServiceLevel\)](<#PrometheusServiceLevel.DeepCopyInto>) + - [func \(in \*PrometheusServiceLevel\) DeepCopyObject\(\) runtime.Object](<#PrometheusServiceLevel.DeepCopyObject>) +- [type PrometheusServiceLevelList](<#PrometheusServiceLevelList>) + - [func \(in \*PrometheusServiceLevelList\) DeepCopy\(\) \*PrometheusServiceLevelList](<#PrometheusServiceLevelList.DeepCopy>) + - [func \(in \*PrometheusServiceLevelList\) DeepCopyInto\(out \*PrometheusServiceLevelList\)](<#PrometheusServiceLevelList.DeepCopyInto>) + - [func \(in \*PrometheusServiceLevelList\) DeepCopyObject\(\) runtime.Object](<#PrometheusServiceLevelList.DeepCopyObject>) +- [type PrometheusServiceLevelSpec](<#PrometheusServiceLevelSpec>) + - [func \(in \*PrometheusServiceLevelSpec\) DeepCopy\(\) \*PrometheusServiceLevelSpec](<#PrometheusServiceLevelSpec.DeepCopy>) + - [func \(in \*PrometheusServiceLevelSpec\) DeepCopyInto\(out \*PrometheusServiceLevelSpec\)](<#PrometheusServiceLevelSpec.DeepCopyInto>) +- [type PrometheusServiceLevelStatus](<#PrometheusServiceLevelStatus>) + - [func \(in \*PrometheusServiceLevelStatus\) DeepCopy\(\) \*PrometheusServiceLevelStatus](<#PrometheusServiceLevelStatus.DeepCopy>) + - [func \(in \*PrometheusServiceLevelStatus\) DeepCopyInto\(out \*PrometheusServiceLevelStatus\)](<#PrometheusServiceLevelStatus.DeepCopyInto>) +- [type SLI](<#SLI>) + - [func \(in \*SLI\) DeepCopy\(\) \*SLI](<#SLI.DeepCopy>) + - [func \(in \*SLI\) DeepCopyInto\(out \*SLI\)](<#SLI.DeepCopyInto>) +- [type SLIEvents](<#SLIEvents>) + - [func \(in \*SLIEvents\) DeepCopy\(\) \*SLIEvents](<#SLIEvents.DeepCopy>) + - [func \(in \*SLIEvents\) DeepCopyInto\(out \*SLIEvents\)](<#SLIEvents.DeepCopyInto>) +- [type SLIPlugin](<#SLIPlugin>) + - [func \(in \*SLIPlugin\) DeepCopy\(\) \*SLIPlugin](<#SLIPlugin.DeepCopy>) + - [func \(in \*SLIPlugin\) DeepCopyInto\(out \*SLIPlugin\)](<#SLIPlugin.DeepCopyInto>) +- [type SLIRaw](<#SLIRaw>) + - [func \(in \*SLIRaw\) DeepCopy\(\) \*SLIRaw](<#SLIRaw.DeepCopy>) + - [func \(in \*SLIRaw\) DeepCopyInto\(out \*SLIRaw\)](<#SLIRaw.DeepCopyInto>) +- [type SLO](<#SLO>) + - [func \(in \*SLO\) DeepCopy\(\) \*SLO](<#SLO.DeepCopy>) + - [func \(in \*SLO\) DeepCopyInto\(out \*SLO\)](<#SLO.DeepCopyInto>) ## Variables + + ```go var ( // SchemeBuilder initializes a scheme builder. @@ -60,13 +62,14 @@ var ( ) ``` -SchemeGroupVersion is group version used to register these objects. +SchemeGroupVersion is group version used to register these objects. ```go var SchemeGroupVersion = schema.GroupVersion{Group: sloth.GroupName, Version: version} ``` -## func Kind + +## func [Kind]() ```go func Kind(kind string) schema.GroupKind @@ -74,7 +77,8 @@ func Kind(kind string) schema.GroupKind Kind takes an unqualified kind and returns back a Group qualified GroupKind. -## func Resource + +## func [Resource]() ```go func Resource(resource string) schema.GroupResource @@ -82,7 +86,8 @@ func Resource(resource string) schema.GroupResource Resource takes an unqualified resource and returns a Group qualified GroupResource. -## func VersionKind + +## func [VersionKind]() ```go func VersionKind(kind string) schema.GroupVersionKind @@ -90,7 +95,8 @@ func VersionKind(kind string) schema.GroupVersionKind VersionKind takes an unqualified kind and returns back a Group qualified GroupVersionKind. -## type Alert + +## type [Alert]() Alert configures specific SLO alert. @@ -111,7 +117,8 @@ type Alert struct { } ``` -### func \(\*Alert\) DeepCopy + +### func \(\*Alert\) [DeepCopy]() ```go func (in *Alert) DeepCopy() *Alert @@ -119,7 +126,8 @@ func (in *Alert) DeepCopy() *Alert DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Alert. -### func \(\*Alert\) DeepCopyInto + +### func \(\*Alert\) [DeepCopyInto]() ```go func (in *Alert) DeepCopyInto(out *Alert) @@ -127,7 +135,8 @@ func (in *Alert) DeepCopyInto(out *Alert) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type Alerting + +## type [Alerting]() Alerting wraps all the configuration required by the SLO alerts. @@ -154,7 +163,8 @@ type Alerting struct { } ``` -### func \(\*Alerting\) DeepCopy + +### func \(\*Alerting\) [DeepCopy]() ```go func (in *Alerting) DeepCopy() *Alerting @@ -162,7 +172,8 @@ func (in *Alerting) DeepCopy() *Alerting DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Alerting. -### func \(\*Alerting\) DeepCopyInto + +### func \(\*Alerting\) [DeepCopyInto]() ```go func (in *Alerting) DeepCopyInto(out *Alerting) @@ -170,7 +181,8 @@ func (in *Alerting) DeepCopyInto(out *Alerting) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type PrometheusServiceLevel + +## type [PrometheusServiceLevel]() \+genclient \+k8s:deepcopy\-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object \+kubebuilder:subresource:status \+kubebuilder:printcolumn:name="SERVICE",type="string",JSONPath=".spec.service" \+kubebuilder:printcolumn:name="DESIRED SLOs",type="integer",JSONPath=".status.processedSLOs" \+kubebuilder:printcolumn:name="READY SLOs",type="integer",JSONPath=".status.promOpRulesGeneratedSLOs" \+kubebuilder:printcolumn:name="GEN OK",type="boolean",JSONPath=".status.promOpRulesGenerated" \+kubebuilder:printcolumn:name="GEN AGE",type="date",JSONPath=".status.lastPromOpRulesSuccessfulGenerated" \+kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" \+kubebuilder:resource:singular=prometheusservicelevel,path=prometheusservicelevels,shortName=psl;pslo,scope=Namespaced,categories=slo;slos;sli;slis @@ -186,7 +198,8 @@ type PrometheusServiceLevel struct { } ``` -### func \(\*PrometheusServiceLevel\) DeepCopy + +### func \(\*PrometheusServiceLevel\) [DeepCopy]() ```go func (in *PrometheusServiceLevel) DeepCopy() *PrometheusServiceLevel @@ -194,7 +207,8 @@ func (in *PrometheusServiceLevel) DeepCopy() *PrometheusServiceLevel DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusServiceLevel. -### func \(\*PrometheusServiceLevel\) DeepCopyInto + +### func \(\*PrometheusServiceLevel\) [DeepCopyInto]() ```go func (in *PrometheusServiceLevel) DeepCopyInto(out *PrometheusServiceLevel) @@ -202,7 +216,8 @@ func (in *PrometheusServiceLevel) DeepCopyInto(out *PrometheusServiceLevel) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -### func \(\*PrometheusServiceLevel\) DeepCopyObject + +### func \(\*PrometheusServiceLevel\) [DeepCopyObject]() ```go func (in *PrometheusServiceLevel) DeepCopyObject() runtime.Object @@ -210,7 +225,8 @@ func (in *PrometheusServiceLevel) DeepCopyObject() runtime.Object DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -## type PrometheusServiceLevelList + +## type [PrometheusServiceLevelList]() \+k8s:deepcopy\-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -225,7 +241,8 @@ type PrometheusServiceLevelList struct { } ``` -### func \(\*PrometheusServiceLevelList\) DeepCopy + +### func \(\*PrometheusServiceLevelList\) [DeepCopy]() ```go func (in *PrometheusServiceLevelList) DeepCopy() *PrometheusServiceLevelList @@ -233,7 +250,8 @@ func (in *PrometheusServiceLevelList) DeepCopy() *PrometheusServiceLevelList DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusServiceLevelList. -### func \(\*PrometheusServiceLevelList\) DeepCopyInto + +### func \(\*PrometheusServiceLevelList\) [DeepCopyInto]() ```go func (in *PrometheusServiceLevelList) DeepCopyInto(out *PrometheusServiceLevelList) @@ -241,7 +259,8 @@ func (in *PrometheusServiceLevelList) DeepCopyInto(out *PrometheusServiceLevelLi DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -### func \(\*PrometheusServiceLevelList\) DeepCopyObject + +### func \(\*PrometheusServiceLevelList\) [DeepCopyObject]() ```go func (in *PrometheusServiceLevelList) DeepCopyObject() runtime.Object @@ -249,7 +268,8 @@ func (in *PrometheusServiceLevelList) DeepCopyObject() runtime.Object DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -## type PrometheusServiceLevelSpec + +## type [PrometheusServiceLevelSpec]() ServiceLevelSpec is the spec for a PrometheusServiceLevel. @@ -271,7 +291,8 @@ type PrometheusServiceLevelSpec struct { } ``` -### func \(\*PrometheusServiceLevelSpec\) DeepCopy + +### func \(\*PrometheusServiceLevelSpec\) [DeepCopy]() ```go func (in *PrometheusServiceLevelSpec) DeepCopy() *PrometheusServiceLevelSpec @@ -279,7 +300,8 @@ func (in *PrometheusServiceLevelSpec) DeepCopy() *PrometheusServiceLevelSpec DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusServiceLevelSpec. -### func \(\*PrometheusServiceLevelSpec\) DeepCopyInto + +### func \(\*PrometheusServiceLevelSpec\) [DeepCopyInto]() ```go func (in *PrometheusServiceLevelSpec) DeepCopyInto(out *PrometheusServiceLevelSpec) @@ -287,7 +309,10 @@ func (in *PrometheusServiceLevelSpec) DeepCopyInto(out *PrometheusServiceLevelSp DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type PrometheusServiceLevelStatus + +## type [PrometheusServiceLevelStatus]() + + ```go type PrometheusServiceLevelStatus struct { @@ -307,7 +332,8 @@ type PrometheusServiceLevelStatus struct { } ``` -### func \(\*PrometheusServiceLevelStatus\) DeepCopy + +### func \(\*PrometheusServiceLevelStatus\) [DeepCopy]() ```go func (in *PrometheusServiceLevelStatus) DeepCopy() *PrometheusServiceLevelStatus @@ -315,7 +341,8 @@ func (in *PrometheusServiceLevelStatus) DeepCopy() *PrometheusServiceLevelStatus DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusServiceLevelStatus. -### func \(\*PrometheusServiceLevelStatus\) DeepCopyInto + +### func \(\*PrometheusServiceLevelStatus\) [DeepCopyInto]() ```go func (in *PrometheusServiceLevelStatus) DeepCopyInto(out *PrometheusServiceLevelStatus) @@ -323,7 +350,8 @@ func (in *PrometheusServiceLevelStatus) DeepCopyInto(out *PrometheusServiceLevel DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type SLI + +## type [SLI]() SLI will tell what is good or bad for the SLO. All SLIs will be get based on time windows, that's why Sloth needs the queries to use \`\{\{.window\}\}\` template variable. @@ -345,7 +373,8 @@ type SLI struct { } ``` -### func \(\*SLI\) DeepCopy + +### func \(\*SLI\) [DeepCopy]() ```go func (in *SLI) DeepCopy() *SLI @@ -353,7 +382,8 @@ func (in *SLI) DeepCopy() *SLI DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLI. -### func \(\*SLI\) DeepCopyInto + +### func \(\*SLI\) [DeepCopyInto]() ```go func (in *SLI) DeepCopyInto(out *SLI) @@ -361,7 +391,8 @@ func (in *SLI) DeepCopyInto(out *SLI) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type SLIEvents + +## type [SLIEvents]() SLIEvents is an SLI that is calculated as the division of bad events and total events, giving a ratio SLI. Normally this is the most common ratio type. @@ -379,7 +410,8 @@ type SLIEvents struct { } ``` -### func \(\*SLIEvents\) DeepCopy + +### func \(\*SLIEvents\) [DeepCopy]() ```go func (in *SLIEvents) DeepCopy() *SLIEvents @@ -387,7 +419,8 @@ func (in *SLIEvents) DeepCopy() *SLIEvents DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLIEvents. -### func \(\*SLIEvents\) DeepCopyInto + +### func \(\*SLIEvents\) [DeepCopyInto]() ```go func (in *SLIEvents) DeepCopyInto(out *SLIEvents) @@ -395,7 +428,8 @@ func (in *SLIEvents) DeepCopyInto(out *SLIEvents) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type SLIPlugin + +## type [SLIPlugin]() SLIPlugin will use the SLI returned by the SLI plugin selected along with the options. @@ -410,7 +444,8 @@ type SLIPlugin struct { } ``` -### func \(\*SLIPlugin\) DeepCopy + +### func \(\*SLIPlugin\) [DeepCopy]() ```go func (in *SLIPlugin) DeepCopy() *SLIPlugin @@ -418,7 +453,8 @@ func (in *SLIPlugin) DeepCopy() *SLIPlugin DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLIPlugin. -### func \(\*SLIPlugin\) DeepCopyInto + +### func \(\*SLIPlugin\) [DeepCopyInto]() ```go func (in *SLIPlugin) DeepCopyInto(out *SLIPlugin) @@ -426,7 +462,8 @@ func (in *SLIPlugin) DeepCopyInto(out *SLIPlugin) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type SLIRaw + +## type [SLIRaw]() SLIRaw is a error ratio SLI already calculated. Normally this will be used when the SLI is already calculated by other recording rule, system... @@ -437,7 +474,8 @@ type SLIRaw struct { } ``` -### func \(\*SLIRaw\) DeepCopy + +### func \(\*SLIRaw\) [DeepCopy]() ```go func (in *SLIRaw) DeepCopy() *SLIRaw @@ -445,7 +483,8 @@ func (in *SLIRaw) DeepCopy() *SLIRaw DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLIRaw. -### func \(\*SLIRaw\) DeepCopyInto + +### func \(\*SLIRaw\) [DeepCopyInto]() ```go func (in *SLIRaw) DeepCopyInto(out *SLIRaw) @@ -453,7 +492,8 @@ func (in *SLIRaw) DeepCopyInto(out *SLIRaw) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type SLO + +## type [SLO]() SLO is the configuration/declaration of the service level objective of a service. @@ -493,7 +533,8 @@ type SLO struct { } ``` -### func \(\*SLO\) DeepCopy + +### func \(\*SLO\) [DeepCopy]() ```go func (in *SLO) DeepCopy() *SLO @@ -501,7 +542,8 @@ func (in *SLO) DeepCopy() *SLO DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLO. -### func \(\*SLO\) DeepCopyInto + +### func \(\*SLO\) [DeepCopyInto]() ```go func (in *SLO) DeepCopyInto(out *SLO) @@ -509,6 +551,4 @@ func (in *SLO) DeepCopyInto(out *SLO) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. - - Generated by [gomarkdoc]() diff --git a/pkg/kubernetes/gen/clientset/versioned/clientset.go b/pkg/kubernetes/gen/clientset/versioned/clientset.go index e70b7571..140005d2 100644 --- a/pkg/kubernetes/gen/clientset/versioned/clientset.go +++ b/pkg/kubernetes/gen/clientset/versioned/clientset.go @@ -3,8 +3,8 @@ package versioned import ( - "fmt" - "net/http" + fmt "fmt" + http "net/http" slothv1 "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1" discovery "k8s.io/client-go/discovery" @@ -17,8 +17,7 @@ type Interface interface { SlothV1() slothv1.SlothV1Interface } -// Clientset contains the clients for groups. Each group has exactly one -// version included in a Clientset. +// Clientset contains the clients for groups. type Clientset struct { *discovery.DiscoveryClient slothV1 *slothv1.SlothV1Client diff --git a/pkg/kubernetes/gen/clientset/versioned/doc.go b/pkg/kubernetes/gen/clientset/versioned/doc.go deleted file mode 100644 index 0e0c2a89..00000000 --- a/pkg/kubernetes/gen/clientset/versioned/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Code generated by client-gen. DO NOT EDIT. - -// This package has the automatically generated clientset. -package versioned diff --git a/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go b/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go index f352624b..a39c56e4 100644 --- a/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go +++ b/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go @@ -15,8 +15,12 @@ import ( // NewSimpleClientset returns a clientset that will respond with the provided objects. // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, -// without applying any validations and/or defaults. It shouldn't be considered a replacement +// without applying any field management, validations and/or defaults. It shouldn't be considered a replacement // for a real clientset and is mostly useful in simple unit tests. +// +// DEPRECATED: NewClientset replaces this with support for field management, which significantly improves +// server side apply testing. NewClientset is only available when apply configurations are generated (e.g. +// via --with-applyconfig). func NewSimpleClientset(objects ...runtime.Object) *Clientset { o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) for _, obj := range objects { diff --git a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_prometheusservicelevel.go b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_prometheusservicelevel.go index fb32ddc2..6e5a861d 100644 --- a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_prometheusservicelevel.go +++ b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_prometheusservicelevel.go @@ -3,124 +3,34 @@ package fake import ( - "context" - - slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - labels "k8s.io/apimachinery/pkg/labels" - schema "k8s.io/apimachinery/pkg/runtime/schema" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - testing "k8s.io/client-go/testing" + v1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" + slothv1 "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1" + gentype "k8s.io/client-go/gentype" ) -// FakePrometheusServiceLevels implements PrometheusServiceLevelInterface -type FakePrometheusServiceLevels struct { +// fakePrometheusServiceLevels implements PrometheusServiceLevelInterface +type fakePrometheusServiceLevels struct { + *gentype.FakeClientWithList[*v1.PrometheusServiceLevel, *v1.PrometheusServiceLevelList] Fake *FakeSlothV1 - ns string -} - -var prometheusservicelevelsResource = schema.GroupVersionResource{Group: "sloth.slok.dev", Version: "v1", Resource: "prometheusservicelevels"} - -var prometheusservicelevelsKind = schema.GroupVersionKind{Group: "sloth.slok.dev", Version: "v1", Kind: "PrometheusServiceLevel"} - -// Get takes name of the prometheusServiceLevel, and returns the corresponding prometheusServiceLevel object, and an error if there is any. -func (c *FakePrometheusServiceLevels) Get(ctx context.Context, name string, options v1.GetOptions) (result *slothv1.PrometheusServiceLevel, err error) { - obj, err := c.Fake. - Invokes(testing.NewGetAction(prometheusservicelevelsResource, c.ns, name), &slothv1.PrometheusServiceLevel{}) - - if obj == nil { - return nil, err - } - return obj.(*slothv1.PrometheusServiceLevel), err -} - -// List takes label and field selectors, and returns the list of PrometheusServiceLevels that match those selectors. -func (c *FakePrometheusServiceLevels) List(ctx context.Context, opts v1.ListOptions) (result *slothv1.PrometheusServiceLevelList, err error) { - obj, err := c.Fake. - Invokes(testing.NewListAction(prometheusservicelevelsResource, prometheusservicelevelsKind, c.ns, opts), &slothv1.PrometheusServiceLevelList{}) - - if obj == nil { - return nil, err - } - - label, _, _ := testing.ExtractFromListOptions(opts) - if label == nil { - label = labels.Everything() - } - list := &slothv1.PrometheusServiceLevelList{ListMeta: obj.(*slothv1.PrometheusServiceLevelList).ListMeta} - for _, item := range obj.(*slothv1.PrometheusServiceLevelList).Items { - if label.Matches(labels.Set(item.Labels)) { - list.Items = append(list.Items, item) - } - } - return list, err -} - -// Watch returns a watch.Interface that watches the requested prometheusServiceLevels. -func (c *FakePrometheusServiceLevels) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { - return c.Fake. - InvokesWatch(testing.NewWatchAction(prometheusservicelevelsResource, c.ns, opts)) - -} - -// Create takes the representation of a prometheusServiceLevel and creates it. Returns the server's representation of the prometheusServiceLevel, and an error, if there is any. -func (c *FakePrometheusServiceLevels) Create(ctx context.Context, prometheusServiceLevel *slothv1.PrometheusServiceLevel, opts v1.CreateOptions) (result *slothv1.PrometheusServiceLevel, err error) { - obj, err := c.Fake. - Invokes(testing.NewCreateAction(prometheusservicelevelsResource, c.ns, prometheusServiceLevel), &slothv1.PrometheusServiceLevel{}) - - if obj == nil { - return nil, err - } - return obj.(*slothv1.PrometheusServiceLevel), err -} - -// Update takes the representation of a prometheusServiceLevel and updates it. Returns the server's representation of the prometheusServiceLevel, and an error, if there is any. -func (c *FakePrometheusServiceLevels) Update(ctx context.Context, prometheusServiceLevel *slothv1.PrometheusServiceLevel, opts v1.UpdateOptions) (result *slothv1.PrometheusServiceLevel, err error) { - obj, err := c.Fake. - Invokes(testing.NewUpdateAction(prometheusservicelevelsResource, c.ns, prometheusServiceLevel), &slothv1.PrometheusServiceLevel{}) - - if obj == nil { - return nil, err - } - return obj.(*slothv1.PrometheusServiceLevel), err -} - -// UpdateStatus was generated because the type contains a Status member. -// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). -func (c *FakePrometheusServiceLevels) UpdateStatus(ctx context.Context, prometheusServiceLevel *slothv1.PrometheusServiceLevel, opts v1.UpdateOptions) (*slothv1.PrometheusServiceLevel, error) { - obj, err := c.Fake. - Invokes(testing.NewUpdateSubresourceAction(prometheusservicelevelsResource, "status", c.ns, prometheusServiceLevel), &slothv1.PrometheusServiceLevel{}) - - if obj == nil { - return nil, err - } - return obj.(*slothv1.PrometheusServiceLevel), err -} - -// Delete takes name of the prometheusServiceLevel and deletes it. Returns an error if one occurs. -func (c *FakePrometheusServiceLevels) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { - _, err := c.Fake. - Invokes(testing.NewDeleteActionWithOptions(prometheusservicelevelsResource, c.ns, name, opts), &slothv1.PrometheusServiceLevel{}) - - return err } -// DeleteCollection deletes a collection of objects. -func (c *FakePrometheusServiceLevels) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { - action := testing.NewDeleteCollectionAction(prometheusservicelevelsResource, c.ns, listOpts) - - _, err := c.Fake.Invokes(action, &slothv1.PrometheusServiceLevelList{}) - return err -} - -// Patch applies the patch and returns the patched prometheusServiceLevel. -func (c *FakePrometheusServiceLevels) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *slothv1.PrometheusServiceLevel, err error) { - obj, err := c.Fake. - Invokes(testing.NewPatchSubresourceAction(prometheusservicelevelsResource, c.ns, name, pt, data, subresources...), &slothv1.PrometheusServiceLevel{}) - - if obj == nil { - return nil, err +func newFakePrometheusServiceLevels(fake *FakeSlothV1, namespace string) slothv1.PrometheusServiceLevelInterface { + return &fakePrometheusServiceLevels{ + gentype.NewFakeClientWithList[*v1.PrometheusServiceLevel, *v1.PrometheusServiceLevelList]( + fake.Fake, + namespace, + v1.SchemeGroupVersion.WithResource("prometheusservicelevels"), + v1.SchemeGroupVersion.WithKind("PrometheusServiceLevel"), + func() *v1.PrometheusServiceLevel { return &v1.PrometheusServiceLevel{} }, + func() *v1.PrometheusServiceLevelList { return &v1.PrometheusServiceLevelList{} }, + func(dst, src *v1.PrometheusServiceLevelList) { dst.ListMeta = src.ListMeta }, + func(list *v1.PrometheusServiceLevelList) []*v1.PrometheusServiceLevel { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1.PrometheusServiceLevelList, items []*v1.PrometheusServiceLevel) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, } - return obj.(*slothv1.PrometheusServiceLevel), err } diff --git a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_sloth_client.go b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_sloth_client.go index 81c230ad..3fa4c757 100644 --- a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_sloth_client.go +++ b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_sloth_client.go @@ -13,7 +13,7 @@ type FakeSlothV1 struct { } func (c *FakeSlothV1) PrometheusServiceLevels(namespace string) v1.PrometheusServiceLevelInterface { - return &FakePrometheusServiceLevels{c, namespace} + return newFakePrometheusServiceLevels(c, namespace) } // RESTClient returns a RESTClient that is used to communicate diff --git a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/prometheusservicelevel.go b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/prometheusservicelevel.go index ee2c8102..8fd3a334 100644 --- a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/prometheusservicelevel.go +++ b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/prometheusservicelevel.go @@ -3,15 +3,14 @@ package v1 import ( - "context" - "time" + context "context" - v1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" + slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" scheme "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/scheme" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" watch "k8s.io/apimachinery/pkg/watch" - rest "k8s.io/client-go/rest" + gentype "k8s.io/client-go/gentype" ) // PrometheusServiceLevelsGetter has a method to return a PrometheusServiceLevelInterface. @@ -22,158 +21,34 @@ type PrometheusServiceLevelsGetter interface { // PrometheusServiceLevelInterface has methods to work with PrometheusServiceLevel resources. type PrometheusServiceLevelInterface interface { - Create(ctx context.Context, prometheusServiceLevel *v1.PrometheusServiceLevel, opts metav1.CreateOptions) (*v1.PrometheusServiceLevel, error) - Update(ctx context.Context, prometheusServiceLevel *v1.PrometheusServiceLevel, opts metav1.UpdateOptions) (*v1.PrometheusServiceLevel, error) - UpdateStatus(ctx context.Context, prometheusServiceLevel *v1.PrometheusServiceLevel, opts metav1.UpdateOptions) (*v1.PrometheusServiceLevel, error) + Create(ctx context.Context, prometheusServiceLevel *slothv1.PrometheusServiceLevel, opts metav1.CreateOptions) (*slothv1.PrometheusServiceLevel, error) + Update(ctx context.Context, prometheusServiceLevel *slothv1.PrometheusServiceLevel, opts metav1.UpdateOptions) (*slothv1.PrometheusServiceLevel, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, prometheusServiceLevel *slothv1.PrometheusServiceLevel, opts metav1.UpdateOptions) (*slothv1.PrometheusServiceLevel, error) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error - Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.PrometheusServiceLevel, error) - List(ctx context.Context, opts metav1.ListOptions) (*v1.PrometheusServiceLevelList, error) + Get(ctx context.Context, name string, opts metav1.GetOptions) (*slothv1.PrometheusServiceLevel, error) + List(ctx context.Context, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.PrometheusServiceLevel, err error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *slothv1.PrometheusServiceLevel, err error) PrometheusServiceLevelExpansion } // prometheusServiceLevels implements PrometheusServiceLevelInterface type prometheusServiceLevels struct { - client rest.Interface - ns string + *gentype.ClientWithList[*slothv1.PrometheusServiceLevel, *slothv1.PrometheusServiceLevelList] } // newPrometheusServiceLevels returns a PrometheusServiceLevels func newPrometheusServiceLevels(c *SlothV1Client, namespace string) *prometheusServiceLevels { return &prometheusServiceLevels{ - client: c.RESTClient(), - ns: namespace, + gentype.NewClientWithList[*slothv1.PrometheusServiceLevel, *slothv1.PrometheusServiceLevelList]( + "prometheusservicelevels", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *slothv1.PrometheusServiceLevel { return &slothv1.PrometheusServiceLevel{} }, + func() *slothv1.PrometheusServiceLevelList { return &slothv1.PrometheusServiceLevelList{} }, + ), } } - -// Get takes name of the prometheusServiceLevel, and returns the corresponding prometheusServiceLevel object, and an error if there is any. -func (c *prometheusServiceLevels) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.PrometheusServiceLevel, err error) { - result = &v1.PrometheusServiceLevel{} - err = c.client.Get(). - Namespace(c.ns). - Resource("prometheusservicelevels"). - Name(name). - VersionedParams(&options, scheme.ParameterCodec). - Do(ctx). - Into(result) - return -} - -// List takes label and field selectors, and returns the list of PrometheusServiceLevels that match those selectors. -func (c *prometheusServiceLevels) List(ctx context.Context, opts metav1.ListOptions) (result *v1.PrometheusServiceLevelList, err error) { - var timeout time.Duration - if opts.TimeoutSeconds != nil { - timeout = time.Duration(*opts.TimeoutSeconds) * time.Second - } - result = &v1.PrometheusServiceLevelList{} - err = c.client.Get(). - Namespace(c.ns). - Resource("prometheusservicelevels"). - VersionedParams(&opts, scheme.ParameterCodec). - Timeout(timeout). - Do(ctx). - Into(result) - return -} - -// Watch returns a watch.Interface that watches the requested prometheusServiceLevels. -func (c *prometheusServiceLevels) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { - var timeout time.Duration - if opts.TimeoutSeconds != nil { - timeout = time.Duration(*opts.TimeoutSeconds) * time.Second - } - opts.Watch = true - return c.client.Get(). - Namespace(c.ns). - Resource("prometheusservicelevels"). - VersionedParams(&opts, scheme.ParameterCodec). - Timeout(timeout). - Watch(ctx) -} - -// Create takes the representation of a prometheusServiceLevel and creates it. Returns the server's representation of the prometheusServiceLevel, and an error, if there is any. -func (c *prometheusServiceLevels) Create(ctx context.Context, prometheusServiceLevel *v1.PrometheusServiceLevel, opts metav1.CreateOptions) (result *v1.PrometheusServiceLevel, err error) { - result = &v1.PrometheusServiceLevel{} - err = c.client.Post(). - Namespace(c.ns). - Resource("prometheusservicelevels"). - VersionedParams(&opts, scheme.ParameterCodec). - Body(prometheusServiceLevel). - Do(ctx). - Into(result) - return -} - -// Update takes the representation of a prometheusServiceLevel and updates it. Returns the server's representation of the prometheusServiceLevel, and an error, if there is any. -func (c *prometheusServiceLevels) Update(ctx context.Context, prometheusServiceLevel *v1.PrometheusServiceLevel, opts metav1.UpdateOptions) (result *v1.PrometheusServiceLevel, err error) { - result = &v1.PrometheusServiceLevel{} - err = c.client.Put(). - Namespace(c.ns). - Resource("prometheusservicelevels"). - Name(prometheusServiceLevel.Name). - VersionedParams(&opts, scheme.ParameterCodec). - Body(prometheusServiceLevel). - Do(ctx). - Into(result) - return -} - -// UpdateStatus was generated because the type contains a Status member. -// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). -func (c *prometheusServiceLevels) UpdateStatus(ctx context.Context, prometheusServiceLevel *v1.PrometheusServiceLevel, opts metav1.UpdateOptions) (result *v1.PrometheusServiceLevel, err error) { - result = &v1.PrometheusServiceLevel{} - err = c.client.Put(). - Namespace(c.ns). - Resource("prometheusservicelevels"). - Name(prometheusServiceLevel.Name). - SubResource("status"). - VersionedParams(&opts, scheme.ParameterCodec). - Body(prometheusServiceLevel). - Do(ctx). - Into(result) - return -} - -// Delete takes name of the prometheusServiceLevel and deletes it. Returns an error if one occurs. -func (c *prometheusServiceLevels) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { - return c.client.Delete(). - Namespace(c.ns). - Resource("prometheusservicelevels"). - Name(name). - Body(&opts). - Do(ctx). - Error() -} - -// DeleteCollection deletes a collection of objects. -func (c *prometheusServiceLevels) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { - var timeout time.Duration - if listOpts.TimeoutSeconds != nil { - timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second - } - return c.client.Delete(). - Namespace(c.ns). - Resource("prometheusservicelevels"). - VersionedParams(&listOpts, scheme.ParameterCodec). - Timeout(timeout). - Body(&opts). - Do(ctx). - Error() -} - -// Patch applies the patch and returns the patched prometheusServiceLevel. -func (c *prometheusServiceLevels) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.PrometheusServiceLevel, err error) { - result = &v1.PrometheusServiceLevel{} - err = c.client.Patch(pt). - Namespace(c.ns). - Resource("prometheusservicelevels"). - Name(name). - SubResource(subresources...). - VersionedParams(&opts, scheme.ParameterCodec). - Body(data). - Do(ctx). - Into(result) - return -} diff --git a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/sloth_client.go b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/sloth_client.go index ac0fbd46..f9eafea4 100644 --- a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/sloth_client.go +++ b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/sloth_client.go @@ -3,10 +3,10 @@ package v1 import ( - "net/http" + http "net/http" - v1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" - "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/scheme" + slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" + scheme "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/scheme" rest "k8s.io/client-go/rest" ) @@ -69,10 +69,10 @@ func New(c rest.Interface) *SlothV1Client { } func setConfigDefaults(config *rest.Config) error { - gv := v1.SchemeGroupVersion + gv := slothv1.SchemeGroupVersion config.GroupVersion = &gv config.APIPath = "/apis" - config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() + config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion() if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() diff --git a/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml b/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml index 7283e667..83cd4c98 100644 --- a/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml +++ b/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml @@ -4,7 +4,6 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: (devel) - creationTimestamp: null name: prometheusservicelevels.sloth.slok.dev spec: group: sloth.slok.dev @@ -45,18 +44,24 @@ spec: name: v1 schema: openAPIV3Schema: - description: PrometheusServiceLevel is the expected service quality level - using Prometheus as the backend used by Sloth. + description: |- + PrometheusServiceLevel is the expected service quality level using Prometheus + as the backend used by Sloth. properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object @@ -66,8 +71,9 @@ spec: labels: additionalProperties: type: string - description: Labels are the Prometheus labels that will have all the - recording and alerting rules generated for the service SLOs. + description: |- + Labels are the Prometheus labels that will have all the recording + and alerting rules generated for the service SLOs. type: object service: description: Service is the application of the SLOs. @@ -75,18 +81,21 @@ spec: slos: description: SLOs are the SLOs of the service. items: - description: SLO is the configuration/declaration of the service - level objective of a service. + description: |- + SLO is the configuration/declaration of the service level objective of + a service. properties: alerting: - description: Alerting is the configuration with all the things - related with the SLO alerts. + description: |- + Alerting is the configuration with all the things related with the SLO + alerts. properties: annotations: additionalProperties: type: string - description: Annotations are the Prometheus annotations - that will have all the alerts generated by this SLO. + description: |- + Annotations are the Prometheus annotations that will have all the alerts generated by + this SLO. type: object labels: additionalProperties: @@ -109,16 +118,16 @@ spec: for the specific alert. type: object disable: - description: Disable disables the alert and makes Sloth - not generating this alert. This can be helpful for - example to disable ticket(warning) alerts. + description: |- + Disable disables the alert and makes Sloth not generating this alert. This + can be helpful for example to disable ticket(warning) alerts. type: boolean labels: additionalProperties: type: string - description: Labels are the Prometheus labels for the - specific alert. For example can be useful to route - the Page alert to specific Slack channel. + description: |- + Labels are the Prometheus labels for the specific alert. For example can be + useful to route the Page alert to specific Slack channel. type: object type: object ticketAlert: @@ -132,16 +141,16 @@ spec: for the specific alert. type: object disable: - description: Disable disables the alert and makes Sloth - not generating this alert. This can be helpful for - example to disable ticket(warning) alerts. + description: |- + Disable disables the alert and makes Sloth not generating this alert. This + can be helpful for example to disable ticket(warning) alerts. type: boolean labels: additionalProperties: type: string - description: Labels are the Prometheus labels for the - specific alert. For example can be useful to route - the Page alert to specific Slack channel. + description: |- + Labels are the Prometheus labels for the specific alert. For example can be + useful to route the Page alert to specific Slack channel. type: object type: object type: object @@ -151,9 +160,10 @@ spec: labels: additionalProperties: type: string - description: Labels are the Prometheus labels that will have - all the recording and alerting rules for this specific SLO. - These labels are merged with the previous level labels. + description: |- + Labels are the Prometheus labels that will have all the recording and + alerting rules for this specific SLO. These labels are merged with the + previous level labels. type: object name: description: Name is the name of the SLO. @@ -171,16 +181,16 @@ spec: description: Events is the events SLI type. properties: errorQuery: - description: ErrorQuery is a Prometheus query that will - get the number/count of events that we consider that - are bad for the SLO (e.g "http 5xx", "latency > 250ms"...). + description: |- + ErrorQuery is a Prometheus query that will get the number/count of events + that we consider that are bad for the SLO (e.g "http 5xx", "latency > 250ms"...). Requires the usage of `{{.window}}` template variable. type: string totalQuery: - description: TotalQuery is a Prometheus query that will - get the total number/count of events for the SLO (e.g - "all http requests"...). Requires the usage of `{{.window}}` - template variable. + description: |- + TotalQuery is a Prometheus query that will get the total number/count of events + for the SLO (e.g "all http requests"...). + Requires the usage of `{{.window}}` template variable. type: string required: - errorQuery @@ -231,9 +241,9 @@ spec: format: date-time type: string observedGeneration: - description: ObservedGeneration tells the generation was acted on, - normally this is required to stop an infinite loop when the status - is updated because it sends a watch updated event to the watchers + description: |- + ObservedGeneration tells the generation was acted on, normally this is required to stop an + infinite loop when the status is updated because it sends a watch updated event to the watchers of the K8s object. format: int64 type: integer diff --git a/pkg/kubernetes/gen/informers/externalversions/factory.go b/pkg/kubernetes/gen/informers/externalversions/factory.go new file mode 100644 index 00000000..988d01eb --- /dev/null +++ b/pkg/kubernetes/gen/informers/externalversions/factory.go @@ -0,0 +1,246 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + reflect "reflect" + sync "sync" + time "time" + + versioned "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" + internalinterfaces "github.com/slok/sloth/pkg/kubernetes/gen/informers/externalversions/internalinterfaces" + sloth "github.com/slok/sloth/pkg/kubernetes/gen/informers/externalversions/sloth" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// SharedInformerOption defines the functional option type for SharedInformerFactory. +type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory + +type sharedInformerFactory struct { + client versioned.Interface + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc + lock sync.Mutex + defaultResync time.Duration + customResync map[reflect.Type]time.Duration + transform cache.TransformFunc + + informers map[reflect.Type]cache.SharedIndexInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[reflect.Type]bool + // wg tracks how many goroutines were started. + wg sync.WaitGroup + // shuttingDown is true when Shutdown has been called. It may still be running + // because it needs to wait for goroutines. + shuttingDown bool +} + +// WithCustomResyncConfig sets a custom resync period for the specified informer types. +func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + for k, v := range resyncConfig { + factory.customResync[reflect.TypeOf(k)] = v + } + return factory + } +} + +// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. +func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.tweakListOptions = tweakListOptions + return factory + } +} + +// WithNamespace limits the SharedInformerFactory to the specified namespace. +func WithNamespace(namespace string) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.namespace = namespace + return factory + } +} + +// WithTransform sets a transform on all informers. +func WithTransform(transform cache.TransformFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.transform = transform + return factory + } +} + +// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync) +} + +// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. +// Listers obtained via this SharedInformerFactory will be subject to the same filters +// as specified here. +// Deprecated: Please use NewSharedInformerFactoryWithOptions instead +func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) +} + +// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. +func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { + factory := &sharedInformerFactory{ + client: client, + namespace: v1.NamespaceAll, + defaultResync: defaultResync, + informers: make(map[reflect.Type]cache.SharedIndexInformer), + startedInformers: make(map[reflect.Type]bool), + customResync: make(map[reflect.Type]time.Duration), + } + + // Apply all options + for _, opt := range options { + factory = opt(factory) + } + + return factory +} + +func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + if f.shuttingDown { + return + } + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + f.wg.Add(1) + // We need a new variable in each loop iteration, + // otherwise the goroutine would use the loop variable + // and that keeps changing. + informer := informer + go func() { + defer f.wg.Done() + informer.Run(stopCh) + }() + f.startedInformers[informerType] = true + } + } +} + +func (f *sharedInformerFactory) Shutdown() { + f.lock.Lock() + f.shuttingDown = true + f.lock.Unlock() + + // Will return immediately if there is nothing to wait for. + f.wg.Wait() +} + +func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + informers := func() map[reflect.Type]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[reflect.Type]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer + } + } + return informers + }() + + res := map[reflect.Type]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// InformerFor returns the SharedIndexInformer for obj using an internal +// client. +func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informerType := reflect.TypeOf(obj) + informer, exists := f.informers[informerType] + if exists { + return informer + } + + resyncPeriod, exists := f.customResync[informerType] + if !exists { + resyncPeriod = f.defaultResync + } + + informer = newFunc(f.client, resyncPeriod) + informer.SetTransform(f.transform) + f.informers[informerType] = informer + + return informer +} + +// SharedInformerFactory provides shared informers for resources in all known +// API group versions. +// +// It is typically used like this: +// +// ctx, cancel := context.Background() +// defer cancel() +// factory := NewSharedInformerFactory(client, resyncPeriod) +// defer factory.WaitForStop() // Returns immediately if nothing was started. +// genericInformer := factory.ForResource(resource) +// typedInformer := factory.SomeAPIGroup().V1().SomeType() +// factory.Start(ctx.Done()) // Start processing these informers. +// synced := factory.WaitForCacheSync(ctx.Done()) +// for v, ok := range synced { +// if !ok { +// fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v) +// return +// } +// } +// +// // Creating informers can also be created after Start, but then +// // Start must be called again: +// anotherGenericInformer := factory.ForResource(resource) +// factory.Start(ctx.Done()) +type SharedInformerFactory interface { + internalinterfaces.SharedInformerFactory + + // Start initializes all requested informers. They are handled in goroutines + // which run until the stop channel gets closed. + // Warning: Start does not block. When run in a go-routine, it will race with a later WaitForCacheSync. + Start(stopCh <-chan struct{}) + + // Shutdown marks a factory as shutting down. At that point no new + // informers can be started anymore and Start will return without + // doing anything. + // + // In addition, Shutdown blocks until all goroutines have terminated. For that + // to happen, the close channel(s) that they were started with must be closed, + // either before Shutdown gets called or while it is waiting. + // + // Shutdown may be called multiple times, even concurrently. All such calls will + // block until all goroutines have terminated. + Shutdown() + + // WaitForCacheSync blocks until all started informers' caches were synced + // or the stop channel gets closed. + WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + + // ForResource gives generic access to a shared informer of the matching type. + ForResource(resource schema.GroupVersionResource) (GenericInformer, error) + + // InformerFor returns the SharedIndexInformer for obj using an internal + // client. + InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer + + Sloth() sloth.Interface +} + +func (f *sharedInformerFactory) Sloth() sloth.Interface { + return sloth.New(f, f.namespace, f.tweakListOptions) +} diff --git a/pkg/kubernetes/gen/informers/externalversions/generic.go b/pkg/kubernetes/gen/informers/externalversions/generic.go new file mode 100644 index 00000000..5a63a470 --- /dev/null +++ b/pkg/kubernetes/gen/informers/externalversions/generic.go @@ -0,0 +1,46 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + fmt "fmt" + + v1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// GenericInformer is type of SharedIndexInformer which will locate and delegate to other +// sharedInformers based on type +type GenericInformer interface { + Informer() cache.SharedIndexInformer + Lister() cache.GenericLister +} + +type genericInformer struct { + informer cache.SharedIndexInformer + resource schema.GroupResource +} + +// Informer returns the SharedIndexInformer. +func (f *genericInformer) Informer() cache.SharedIndexInformer { + return f.informer +} + +// Lister returns the GenericLister. +func (f *genericInformer) Lister() cache.GenericLister { + return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) +} + +// ForResource gives generic access to a shared informer of the matching type +// TODO extend this to unknown resources with a client pool +func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { + switch resource { + // Group=sloth.slok.dev, Version=v1 + case v1.SchemeGroupVersion.WithResource("prometheusservicelevels"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Sloth().V1().PrometheusServiceLevels().Informer()}, nil + + } + + return nil, fmt.Errorf("no informer found for %v", resource) +} diff --git a/pkg/kubernetes/gen/informers/externalversions/internalinterfaces/factory_interfaces.go b/pkg/kubernetes/gen/informers/externalversions/internalinterfaces/factory_interfaces.go new file mode 100644 index 00000000..c96f6795 --- /dev/null +++ b/pkg/kubernetes/gen/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -0,0 +1,24 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package internalinterfaces + +import ( + time "time" + + versioned "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" +) + +// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. +type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer + +// SharedInformerFactory a small interface to allow for adding an informer without an import cycle +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer +} + +// TweakListOptionsFunc is a function that transforms a v1.ListOptions. +type TweakListOptionsFunc func(*v1.ListOptions) diff --git a/pkg/kubernetes/gen/informers/externalversions/sloth/interface.go b/pkg/kubernetes/gen/informers/externalversions/sloth/interface.go new file mode 100644 index 00000000..f67ce532 --- /dev/null +++ b/pkg/kubernetes/gen/informers/externalversions/sloth/interface.go @@ -0,0 +1,30 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package sloth + +import ( + internalinterfaces "github.com/slok/sloth/pkg/kubernetes/gen/informers/externalversions/internalinterfaces" + v1 "github.com/slok/sloth/pkg/kubernetes/gen/informers/externalversions/sloth/v1" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1 provides access to shared informers for resources in V1. + V1() v1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1 returns a new v1.Interface. +func (g *group) V1() v1.Interface { + return v1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/kubernetes/gen/informers/externalversions/sloth/v1/interface.go b/pkg/kubernetes/gen/informers/externalversions/sloth/v1/interface.go new file mode 100644 index 00000000..381bcbc4 --- /dev/null +++ b/pkg/kubernetes/gen/informers/externalversions/sloth/v1/interface.go @@ -0,0 +1,29 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package v1 + +import ( + internalinterfaces "github.com/slok/sloth/pkg/kubernetes/gen/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // PrometheusServiceLevels returns a PrometheusServiceLevelInformer. + PrometheusServiceLevels() PrometheusServiceLevelInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// PrometheusServiceLevels returns a PrometheusServiceLevelInformer. +func (v *version) PrometheusServiceLevels() PrometheusServiceLevelInformer { + return &prometheusServiceLevelInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/kubernetes/gen/informers/externalversions/sloth/v1/prometheusservicelevel.go b/pkg/kubernetes/gen/informers/externalversions/sloth/v1/prometheusservicelevel.go new file mode 100644 index 00000000..d8605c40 --- /dev/null +++ b/pkg/kubernetes/gen/informers/externalversions/sloth/v1/prometheusservicelevel.go @@ -0,0 +1,74 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package v1 + +import ( + context "context" + time "time" + + apislothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" + versioned "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" + internalinterfaces "github.com/slok/sloth/pkg/kubernetes/gen/informers/externalversions/internalinterfaces" + slothv1 "github.com/slok/sloth/pkg/kubernetes/gen/listers/sloth/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// PrometheusServiceLevelInformer provides access to a shared informer and lister for +// PrometheusServiceLevels. +type PrometheusServiceLevelInformer interface { + Informer() cache.SharedIndexInformer + Lister() slothv1.PrometheusServiceLevelLister +} + +type prometheusServiceLevelInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewPrometheusServiceLevelInformer constructs a new informer for PrometheusServiceLevel type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewPrometheusServiceLevelInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredPrometheusServiceLevelInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredPrometheusServiceLevelInformer constructs a new informer for PrometheusServiceLevel type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredPrometheusServiceLevelInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SlothV1().PrometheusServiceLevels(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SlothV1().PrometheusServiceLevels(namespace).Watch(context.TODO(), options) + }, + }, + &apislothv1.PrometheusServiceLevel{}, + resyncPeriod, + indexers, + ) +} + +func (f *prometheusServiceLevelInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredPrometheusServiceLevelInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *prometheusServiceLevelInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apislothv1.PrometheusServiceLevel{}, f.defaultInformer) +} + +func (f *prometheusServiceLevelInformer) Lister() slothv1.PrometheusServiceLevelLister { + return slothv1.NewPrometheusServiceLevelLister(f.Informer().GetIndexer()) +} diff --git a/pkg/kubernetes/gen/listers/sloth/v1/expansion_generated.go b/pkg/kubernetes/gen/listers/sloth/v1/expansion_generated.go new file mode 100644 index 00000000..c1c226a6 --- /dev/null +++ b/pkg/kubernetes/gen/listers/sloth/v1/expansion_generated.go @@ -0,0 +1,11 @@ +// Code generated by lister-gen. DO NOT EDIT. + +package v1 + +// PrometheusServiceLevelListerExpansion allows custom methods to be added to +// PrometheusServiceLevelLister. +type PrometheusServiceLevelListerExpansion interface{} + +// PrometheusServiceLevelNamespaceListerExpansion allows custom methods to be added to +// PrometheusServiceLevelNamespaceLister. +type PrometheusServiceLevelNamespaceListerExpansion interface{} diff --git a/pkg/kubernetes/gen/listers/sloth/v1/prometheusservicelevel.go b/pkg/kubernetes/gen/listers/sloth/v1/prometheusservicelevel.go new file mode 100644 index 00000000..00707727 --- /dev/null +++ b/pkg/kubernetes/gen/listers/sloth/v1/prometheusservicelevel.go @@ -0,0 +1,54 @@ +// Code generated by lister-gen. DO NOT EDIT. + +package v1 + +import ( + slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// PrometheusServiceLevelLister helps list PrometheusServiceLevels. +// All objects returned here must be treated as read-only. +type PrometheusServiceLevelLister interface { + // List lists all PrometheusServiceLevels in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*slothv1.PrometheusServiceLevel, err error) + // PrometheusServiceLevels returns an object that can list and get PrometheusServiceLevels. + PrometheusServiceLevels(namespace string) PrometheusServiceLevelNamespaceLister + PrometheusServiceLevelListerExpansion +} + +// prometheusServiceLevelLister implements the PrometheusServiceLevelLister interface. +type prometheusServiceLevelLister struct { + listers.ResourceIndexer[*slothv1.PrometheusServiceLevel] +} + +// NewPrometheusServiceLevelLister returns a new PrometheusServiceLevelLister. +func NewPrometheusServiceLevelLister(indexer cache.Indexer) PrometheusServiceLevelLister { + return &prometheusServiceLevelLister{listers.New[*slothv1.PrometheusServiceLevel](indexer, slothv1.Resource("prometheusservicelevel"))} +} + +// PrometheusServiceLevels returns an object that can list and get PrometheusServiceLevels. +func (s *prometheusServiceLevelLister) PrometheusServiceLevels(namespace string) PrometheusServiceLevelNamespaceLister { + return prometheusServiceLevelNamespaceLister{listers.NewNamespaced[*slothv1.PrometheusServiceLevel](s.ResourceIndexer, namespace)} +} + +// PrometheusServiceLevelNamespaceLister helps list and get PrometheusServiceLevels. +// All objects returned here must be treated as read-only. +type PrometheusServiceLevelNamespaceLister interface { + // List lists all PrometheusServiceLevels in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*slothv1.PrometheusServiceLevel, err error) + // Get retrieves the PrometheusServiceLevel from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*slothv1.PrometheusServiceLevel, error) + PrometheusServiceLevelNamespaceListerExpansion +} + +// prometheusServiceLevelNamespaceLister implements the PrometheusServiceLevelNamespaceLister +// interface. +type prometheusServiceLevelNamespaceLister struct { + listers.ResourceIndexer[*slothv1.PrometheusServiceLevel] +} diff --git a/pkg/prometheus/alertwindows/v1/README.md b/pkg/prometheus/alertwindows/v1/README.md index 09544ab2..2e0017e7 100755 --- a/pkg/prometheus/alertwindows/v1/README.md +++ b/pkg/prometheus/alertwindows/v1/README.md @@ -9,25 +9,32 @@ import "github.com/slok/sloth/pkg/prometheus/alertwindows/v1" ## Index - [Constants](<#constants>) -- [type AlertWindows](<#type-alertwindows>) -- [type PageWindow](<#type-pagewindow>) -- [type QuickSlowWindow](<#type-quickslowwindow>) -- [type Spec](<#type-spec>) -- [type TicketWindow](<#type-ticketwindow>) -- [type Window](<#type-window>) +- [type AlertWindows](<#AlertWindows>) +- [type PageWindow](<#PageWindow>) +- [type QuickSlowWindow](<#QuickSlowWindow>) +- [type Spec](<#Spec>) +- [type TicketWindow](<#TicketWindow>) +- [type Window](<#Window>) ## Constants + + ```go const APIVersion = "sloth.slok.dev/v1" ``` + + ```go const Kind = "AlertWindows" ``` -## type AlertWindows + +## type [AlertWindows]() + + ```go type AlertWindows struct { @@ -37,7 +44,8 @@ type AlertWindows struct { } ``` -## type PageWindow + +## type [PageWindow]() PageWindow represents the configuration for page alerting. @@ -47,7 +55,10 @@ type PageWindow struct { } ``` -## type QuickSlowWindow + +## type [QuickSlowWindow]() + + ```go type QuickSlowWindow struct { @@ -58,7 +69,8 @@ type QuickSlowWindow struct { } ``` -## type Spec + +## type [Spec]() Spec represents the root type of the Alerting window. @@ -73,7 +85,8 @@ type Spec struct { } ``` -## type TicketWindow + +## type [TicketWindow]() PageWindow represents the configuration for ticket alerting. @@ -83,7 +96,10 @@ type TicketWindow struct { } ``` -## type Window + +## type [Window]() + + ```go type Window struct { @@ -97,6 +113,4 @@ type Window struct { } ``` - - Generated by [gomarkdoc]() diff --git a/pkg/prometheus/api/v1/README.md b/pkg/prometheus/api/v1/README.md index ab419d3d..43e4f0f2 100755 --- a/pkg/prometheus/api/v1/README.md +++ b/pkg/prometheus/api/v1/README.md @@ -6,7 +6,7 @@ import "github.com/slok/sloth/pkg/prometheus/api/v1" ``` -### Package v1 +Package v1 Example YAML spec with 2 SLOs: @@ -66,23 +66,26 @@ slos: ## Index - [Constants](<#constants>) -- [type Alert](<#type-alert>) -- [type Alerting](<#type-alerting>) -- [type SLI](<#type-sli>) -- [type SLIEvents](<#type-slievents>) -- [type SLIPlugin](<#type-sliplugin>) -- [type SLIRaw](<#type-sliraw>) -- [type SLO](<#type-slo>) -- [type Spec](<#type-spec>) +- [type Alert](<#Alert>) +- [type Alerting](<#Alerting>) +- [type SLI](<#SLI>) +- [type SLIEvents](<#SLIEvents>) +- [type SLIPlugin](<#SLIPlugin>) +- [type SLIRaw](<#SLIRaw>) +- [type SLO](<#SLO>) +- [type Spec](<#Spec>) ## Constants + + ```go const Version = "prometheus/v1" ``` -## type Alert + +## type [Alert]() Alert configures specific SLO alert. @@ -99,7 +102,8 @@ type Alert struct { } ``` -## type Alerting + +## type [Alerting]() Alerting wraps all the configuration required by the SLO alerts. @@ -119,7 +123,8 @@ type Alerting struct { } ``` -## type SLI + +## type [SLI]() SLI will tell what is good or bad for the SLO. All SLIs will be get based on time windows, that's why Sloth needs the queries to use \`\{\{.window\}\}\` template variable. @@ -136,7 +141,8 @@ type SLI struct { } ``` -## type SLIEvents + +## type [SLIEvents]() SLIEvents is an SLI that is calculated as the division of bad events and total events, giving a ratio SLI. Normally this is the most common ratio type. @@ -153,7 +159,8 @@ type SLIEvents struct { } ``` -## type SLIPlugin + +## type [SLIPlugin]() SLIPlugin will use the SLI returned by the SLI plugin selected along with the options. @@ -166,7 +173,8 @@ type SLIPlugin struct { } ``` -## type SLIRaw + +## type [SLIRaw]() SLIRaw is a error ratio SLI already calculated. Normally this will be used when the SLI is already calculated by other recording rule, system... @@ -177,7 +185,8 @@ type SLIRaw struct { } ``` -## type SLO + +## type [SLO]() SLO is the configuration/declaration of the service level objective of a service. @@ -201,7 +210,8 @@ type SLO struct { } ``` -## type Spec + +## type [Spec]() Spec represents the root type of the SLOs declaration specification. @@ -219,6 +229,4 @@ type Spec struct { } ``` - - Generated by [gomarkdoc]() diff --git a/scripts/kubegen.sh b/scripts/kubegen.sh index fb1cd0b2..2aee624e 100755 --- a/scripts/kubegen.sh +++ b/scripts/kubegen.sh @@ -3,33 +3,16 @@ set -o errexit set -o nounset -IMAGE_CLI_GEN=quay.io/slok/kube-code-generator:v1.25.0 -IMAGE_CRD_GEN=quay.io/slok/kube-code-generator:v1.25.0 -ROOT_DIRECTORY=$(dirname "$(readlink -f "$0")")/../ -PROJECT_PACKAGE="github.com/slok/sloth" +IMAGE_GEN=ghcr.io/slok/kube-code-generator:v0.6.0 GEN_DIRECTORY="pkg/kubernetes/gen" echo "Cleaning gen directory" rm -rf ./${GEN_DIRECTORY} -echo "Generating Kubernetes CRD clients..." -docker run -it --rm \ - -v ${ROOT_DIRECTORY}:/go/src/${PROJECT_PACKAGE} \ - -e PROJECT_PACKAGE=${PROJECT_PACKAGE} \ - -e CLIENT_GENERATOR_OUT=${PROJECT_PACKAGE}/pkg/kubernetes/gen \ - -e APIS_ROOT=${PROJECT_PACKAGE}/pkg/kubernetes/api \ - -e GROUPS_VERSION="sloth:v1" \ - -e GENERATION_TARGETS="deepcopy,client" \ - ${IMAGE_CLI_GEN} - -echo "Generating Kubernetes CRD manifests..." -docker run -it --rm \ - -v ${ROOT_DIRECTORY}:/src \ - -e GO_PROJECT_ROOT=/src \ - -e CRD_FLAG="crd:crdVersions=v1,allowDangerousTypes=true" \ - -e CRD_TYPES_PATH=/src/pkg/kubernetes/api \ - -e CRD_OUT_PATH=/src/pkg/kubernetes/gen/crd \ - ${IMAGE_CRD_GEN} update-crd.sh +docker run --rm -it -v ${PWD}:/app "${IMAGE_GEN}" \ + --apis-in ./pkg/kubernetes/api \ + --go-gen-out ./${GEN_DIRECTORY} \ + --crd-gen-out ./${GEN_DIRECTORY}/crd echo "Copying crd to helm chart..." rm ./deploy/kubernetes/helm/sloth/crds/* From 5e0f1fcd3f8fccacef9b2ce113fe893b0ab1b1c3 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sun, 23 Mar 2025 12:59:46 +0100 Subject: [PATCH 009/173] Update CI and some docs Signed-off-by: Xabier Larrakoetxea --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/helmrelease.yaml | 2 ++ CHANGELOG.md | 4 ++++ README.md | 8 +++++++- go.mod | 2 -- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8b762524..3cae4f7d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,7 +17,7 @@ jobs: ./scripts/check/check.sh unit-test: - name: Unit testq + name: Unit test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -132,7 +132,7 @@ jobs: tagged-release-images: # Only on tags. - if: startsWith(github.ref, 'refs/tags/') + if: startsWith(github.ref, 'refs/tags/v') env: PROD_IMAGE_NAME: ghcr.io/${GITHUB_REPOSITORY} needs: diff --git a/.github/workflows/helmrelease.yaml b/.github/workflows/helmrelease.yaml index 5ebb8595..ce15d33b 100644 --- a/.github/workflows/helmrelease.yaml +++ b/.github/workflows/helmrelease.yaml @@ -10,6 +10,8 @@ on: jobs: release: runs-on: ubuntu-latest + # Only on helm tags. + if: startsWith(github.ref, 'refs/tags/helm-') steps: - name: Checkout uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 41828fc2..0a7e8d79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - Update to Kubernetes v1.32. - Update all other dependencies to latest versions. +### Fixes + +- Allow spec files with CRLF. + ## [v0.11.0] - 2022-10-22 ### Changed diff --git a/README.md b/README.md index bbcafe10..7609429d 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,15 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/slok/sloth)](https://goreportcard.com/report/github.com/slok/sloth) [![Apache 2 licensed](https://img.shields.io/badge/license-Apache2-blue.svg)](https://raw.githubusercontent.com/slok/sloth/master/LICENSE) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/slok/sloth)](https://github.com/slok/sloth/releases/latest) -![Kubernetes release](https://img.shields.io/badge/Kubernetes-v1.25-green?logo=Kubernetes&style=flat&color=326CE5&logoColor=white) +![Kubernetes release](https://img.shields.io/badge/Kubernetes-v1.32-green?logo=Kubernetes&style=flat&color=326CE5&logoColor=white) [![OpenSLO](https://img.shields.io/badge/OpenSLO-v1alpha-green?color=4974EA&style=flat)](https://github.com/OpenSLO/OpenSLO#slo) +## Project status + +Sloth is [still maintained](https://github.com/slok/sloth/issues/521#issuecomment-2745271807). + +After a period of inactivity, the project is now being updated. It will take some time to catch up, but we’ll get there. More updates coming in the next few weeks. + ## Introduction Meet the easiest way to generate [SLOs][google-slo] for Prometheus. diff --git a/go.mod b/go.mod index 2e55c1ac..a5090537 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module github.com/slok/sloth go 1.24 -toolchain go1.24.0 - require ( github.com/OpenSLO/oslo v0.12.0 github.com/alecthomas/kingpin/v2 v2.4.0 From a480341a8b81e80fda98e28c281856e4e880e5ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 07:34:42 +0000 Subject: [PATCH 010/173] build(deps): bump golang in /docker/prod Bumps golang from 1.24.0-alpine to 1.24.1-alpine. --- updated-dependencies: - dependency-name: golang dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docker/prod/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 430a4cf8..a54801ad 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,7 +1,7 @@ # Set also `ARCH` ARG here so we can use it on all the `FROM`s. ARG ARCH -FROM golang:1.24.0-alpine as build-stage +FROM golang:1.24.1-alpine as build-stage LABEL org.opencontainers.image.source https://github.com/slok/sloth From 437c93c84e4f528ce063e322af9ef1afc0a19e9d Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Tue, 25 Mar 2025 10:17:53 +0100 Subject: [PATCH 011/173] Update helm test dependencies Signed-off-by: Xabier Larrakoetxea --- deploy/kubernetes/helm/sloth/tests/go.mod | 218 ++-- deploy/kubernetes/helm/sloth/tests/go.sum | 1214 ++++++--------------- 2 files changed, 466 insertions(+), 966 deletions(-) diff --git a/deploy/kubernetes/helm/sloth/tests/go.mod b/deploy/kubernetes/helm/sloth/tests/go.mod index 3347b52c..6b224593 100644 --- a/deploy/kubernetes/helm/sloth/tests/go.mod +++ b/deploy/kubernetes/helm/sloth/tests/go.mod @@ -1,139 +1,151 @@ module github.com/slok/sloth/helm -go 1.19 +go 1.24.0 require ( - github.com/slok/go-helm-template v0.3.0 - github.com/stretchr/testify v1.8.0 + github.com/slok/go-helm-template v0.8.0 + github.com/stretchr/testify v1.10.0 ) require ( - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/BurntSushi/toml v1.1.0 // indirect + dario.cat/mergo v1.0.1 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/BurntSushi/toml v1.5.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.1.1 // indirect - github.com/Masterminds/sprig/v3 v3.2.2 // indirect - github.com/Masterminds/squirrel v1.5.3 // indirect - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect - github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/containerd/containerd v1.6.6 // indirect - github.com/cyphar/filepath-securejoin v0.2.3 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/cli v20.10.17+incompatible // indirect - github.com/docker/distribution v2.8.1+incompatible // indirect - github.com/docker/docker v20.10.17+incompatible // indirect - github.com/docker/docker-credential-helpers v0.6.4 // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.3 // indirect + github.com/containerd/containerd v1.7.27 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v28.0.2+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker v28.0.2+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-metrics v0.0.1 // indirect - github.com/docker/go-units v0.4.0 // indirect - github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1 // indirect - github.com/emicklei/go-restful/v3 v3.8.0 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect - github.com/fatih/color v1.13.0 // indirect - github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 // indirect - github.com/go-gorp/gorp/v3 v3.0.2 // indirect - github.com/go-logr/logr v1.2.3 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.5 // indirect - github.com/go-openapi/swag v0.19.14 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/btree v1.0.1 // indirect - github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/google/go-cmp v0.5.8 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.2.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/gosuri/uitable v0.0.4 // indirect - github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect - github.com/huandu/xstrings v1.3.2 // indirect - github.com/imdario/mergo v0.3.12 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/jmoiron/sqlx v1.3.5 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.13.6 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/lib/pq v1.10.6 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mailru/easyjson v0.7.6 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect - github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect - github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.12.1 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect - github.com/prometheus/procfs v0.7.3 // indirect - github.com/rubenv/sql-migrate v1.1.2 // indirect - github.com/russross/blackfriday v1.5.2 // indirect - github.com/shopspring/decimal v1.2.0 // indirect - github.com/sirupsen/logrus v1.8.1 // indirect - github.com/spf13/cast v1.4.1 // indirect - github.com/spf13/cobra v1.5.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.21.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rubenv/sql-migrate v1.7.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/xlab/treeprint v1.1.0 // indirect - go.etcd.io/etcd/api/v3 v3.5.4 // indirect - go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect - golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect - golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect - google.golang.org/grpc v1.47.0 // indirect - google.golang.org/protobuf v1.28.0 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect + google.golang.org/grpc v1.71.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.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 - helm.sh/helm/v3 v3.10.1 // indirect - k8s.io/api v0.25.2 // indirect - k8s.io/apiextensions-apiserver v0.25.2 // indirect - k8s.io/apimachinery v0.25.2 // indirect - k8s.io/apiserver v0.25.2 // indirect - k8s.io/cli-runtime v0.25.2 // indirect - k8s.io/client-go v0.25.2 // indirect - k8s.io/component-base v0.25.2 // indirect - k8s.io/klog/v2 v2.70.1 // indirect - k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect - k8s.io/kubectl v0.25.2 // indirect - k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect - oras.land/oras-go v1.2.0 // indirect - sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect - sigs.k8s.io/kustomize/api v0.12.1 // indirect - sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + helm.sh/helm/v3 v3.17.2 // indirect + k8s.io/api v0.32.3 // indirect + k8s.io/apiextensions-apiserver v0.32.3 // indirect + k8s.io/apimachinery v0.32.3 // indirect + k8s.io/apiserver v0.32.3 // indirect + k8s.io/cli-runtime v0.32.3 // indirect + k8s.io/client-go v0.32.3 // indirect + k8s.io/component-base v0.32.3 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/kubectl v0.32.3 // indirect + k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect + oras.land/oras-go v1.2.6 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/kustomize/api v0.19.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/deploy/kubernetes/helm/sloth/tests/go.sum b/deploy/kubernetes/helm/sloth/tests/go.sum index 1933aab8..12745c67 100644 --- a/deploy/kubernetes/helm/sloth/tests/go.sum +++ b/deploy/kubernetes/helm/sloth/tests/go.sum @@ -1,426 +1,248 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= -github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= -github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= -github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvdHpkZc= -github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= -github.com/Microsoft/hcsshim v0.9.3 h1:k371PzBuRrz2b+ebGuI2nVgVhgsVX60jMfSw80NECxo= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.12.3 h1:LS9NXqXhMoqNCplK1ApmVSfB4UnVLRDWRapB6EIlxE0= +github.com/Microsoft/hcsshim v0.12.3/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= -github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= -github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/containerd/cgroups v1.0.3 h1:ADZftAkglvCiD44c77s5YmMqaP2pzVCFZvBmAlBdAP4= -github.com/containerd/containerd v1.6.6 h1:xJNPhbrmz8xAMDNoVjHy9YHtWwEQNS+CDkcIRh7t8Y0= -github.com/containerd/containerd v1.6.6/go.mod h1:ZoP1geJldzCVY3Tonoz7b1IXk8rIX0Nltt5QE4OMNk0= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= -github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= +github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0= +github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= +github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= +github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= +github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= +github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 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/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/distribution/distribution/v3 v3.0.0-20220526142353-ffbd94cbe269 h1:hbCT8ZPPMqefiAWD2ZKjn7ypokIGViTvBBg/ExLSdCk= -github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M= -github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= -github.com/docker/docker v20.10.17+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= -github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/distribution/v3 v3.0.0-beta.1 h1:X+ELTxPuZ1Xe5MsD3kp2wfGUhc8I+MPfRis8dZ818Ic= +github.com/distribution/distribution/v3 v3.0.0-beta.1/go.mod h1:O9O8uamhHzWWQVTjuQpyYUVm/ShPHPUDgvQMpHGVBDs= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v28.0.2+incompatible h1:cRPZ77FK3/IXTAIQQj1vmhlxiLS5m+MIUDwS6f57lrE= +github.com/docker/cli v28.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8= +github.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1 h1:yY9rWGoXv1U5pl4gxqlULARMQD7x0QG85lqEXTWysik= -github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= -github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= -github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= -github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0 h1:skJKxRtNmevLqnayafdLe2AsenqRupVmzZSqrvb5caU= -github.com/go-errors/errors v1.0.2-0.20180813162953-d98b870cc4e0/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gorp/gorp/v3 v3.0.2 h1:ULqJXIekoqMx29FI5ekXXFoH1dT2Vc8UhnRzBg+Emz4= -github.com/go-gorp/gorp/v3 v3.0.2/go.mod h1:BJ3q1ejpV8cVALtcXvXaXyTOlMmJhWDxTmncaR6rwBY= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= -github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= -github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= -github.com/gobuffalo/packd v1.0.1/go.mod h1:PP2POP3p3RXGz7Jh6eYEf93S7vA2za6xM7QT85L4+VY= -github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= -github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= -github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= -github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= -github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= -github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= -github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= -github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= -github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= -github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= 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= @@ -430,591 +252,257 @@ 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU= -github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1 h1:oL4IBbcqwhhNWh31bjOX8C/OCy0zs9906d/VUru+bqg= -github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= +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/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= +github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rubenv/sql-migrate v1.1.2 h1:9M6oj4e//owVVHYrFISmY9LBRw6gzkCNmD9MV36tZeQ= -github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= +github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= +github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= +github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= +github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/slok/go-helm-template v0.3.0 h1:nQEIZMDiXkF1BFoIsfQ4TuKf2wtQZ3cBLbkBOM8fZeI= -github.com/slok/go-helm-template v0.3.0/go.mod h1:yqHrzAOWEOU4G46n7XGrCae4bCI84CsB34ZeScqTseM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -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/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slok/go-helm-template v0.8.0 h1:rERtZLpbwGpeiAd9RaHPoshGL821obT/cBhfxkayXdo= +github.com/slok/go-helm-template v0.8.0/go.mod h1:RB11tKCmcV/3/oKRuxCfM7ImumY8kZ7vVluetwBVc/c= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 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 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/xlab/treeprint v1.1.0 h1:G/1DjNkPpfZCFt9CSh6b5/nY4VimlbHF3Rh4obvtzDk= -github.com/xlab/treeprint v1.1.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= -github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= -github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= -go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= -go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 h1:ysCfPZB9AjUlMa1UHYup3c9dAOCMQX/6sxSfPBUoxHw= +go.opentelemetry.io/contrib/exporters/autoexport v0.46.1/go.mod h1:ha0aiYm+DOPsLHjh0zoQ8W8sLT+LJ58J3j47lGpSLrU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 h1:bflGWrfYyuulcdxf14V6n9+CoQcu5SAAdHmDPAJnlps= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0/go.mod h1:qcTO4xHAxZLaLxPd60TdE88rxtItPHgHWqOhOGRr0as= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= +go.opentelemetry.io/otel/exporters/prometheus v0.44.0 h1:08qeJgaPC0YEBu2PQMbqU3rogTlyzpjhCI2b58Yn00w= +go.opentelemetry.io/otel/exporters/prometheus v0.44.0/go.mod h1:ERL2uIeBtg4TxZdojHUwzZfIFlUIjZtxubT5p4h1Gjg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 h1:dEZWPjVN22urgYCza3PXRUGEyCB++y1sAqm6guWFesk= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0/go.mod h1:sTt30Evb7hJB/gEk27qLb1+l9n4Tb8HvHkR0Wx3S6CU= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 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.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 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= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 h1:hrbNEivu7Zn1pxvHk6MBrq9iE22woVILTHqexqBxe6I= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -github.com/alecthomas/kingpin/v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= +google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/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.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -helm.sh/helm/v3 v3.10.1 h1:uTnNlYx8QcTSNA4ZJ50Llwife4CSohUY4ehumyVf2QE= -helm.sh/helm/v3 v3.10.1/go.mod h1:CXOcs02AYvrlPMWARNYNRgf2rNP7gLJQsi/Ubd4EDrI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.25.2 h1:v6G8RyFcwf0HR5jQGIAYlvtRNrxMJQG1xJzaSeVnIS8= -k8s.io/api v0.25.2/go.mod h1:qP1Rn4sCVFwx/xIhe+we2cwBLTXNcheRyYXwajonhy0= -k8s.io/apiextensions-apiserver v0.25.2 h1:8uOQX17RE7XL02ngtnh3TgifY7EhekpK+/piwzQNnBo= -k8s.io/apiextensions-apiserver v0.25.2/go.mod h1:iRwwRDlWPfaHhuBfQ0WMa5skdQfrE18QXJaJvIDLvE8= -k8s.io/apimachinery v0.25.2 h1:WbxfAjCx+AeN8Ilp9joWnyJ6xu9OMeS/fsfjK/5zaQs= -k8s.io/apimachinery v0.25.2/go.mod h1:hqqA1X0bsgsxI6dXsJ4HnNTBOmJNxyPp8dw3u2fSHwA= -k8s.io/apiserver v0.25.2 h1:YePimobk187IMIdnmsMxsfIbC5p4eX3WSOrS9x6FEYw= -k8s.io/apiserver v0.25.2/go.mod h1:30r7xyQTREWCkG2uSjgjhQcKVvAAlqoD+YyrqR6Cn+I= -k8s.io/cli-runtime v0.25.2 h1:XOx+SKRjBpYMLY/J292BHTkmyDffl/qOx3YSuFZkTuc= -k8s.io/cli-runtime v0.25.2/go.mod h1:OQx3+/0st6x5YpkkJQlEWLC73V0wHsOFMC1/roxV8Oc= -k8s.io/client-go v0.25.2 h1:SUPp9p5CwM0yXGQrwYurw9LWz+YtMwhWd0GqOsSiefo= -k8s.io/client-go v0.25.2/go.mod h1:i7cNU7N+yGQmJkewcRD2+Vuj4iz7b30kI8OcL3horQ4= -k8s.io/component-base v0.25.2 h1:Nve/ZyHLUBHz1rqwkjXm/Re6IniNa5k7KgzxZpTfSQY= -k8s.io/component-base v0.25.2/go.mod h1:90W21YMr+Yjg7MX+DohmZLzjsBtaxQDDwaX4YxDkl60= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= -k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= -k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= -k8s.io/kubectl v0.25.2 h1:2993lTeVimxKSWx/7z2PiJxUILygRa3tmC4QhFaeioA= -k8s.io/kubectl v0.25.2/go.mod h1:eoBGJtKUj7x38KXelz+dqVtbtbKwCqyKzJWmBHU0prg= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= -k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -oras.land/oras-go v1.2.0 h1:yoKosVIbsPoFMqAIFHTnrmOuafHal+J/r+I5bdbVWu4= -oras.land/oras-go v1.2.0/go.mod h1:pFNs7oHp2dYsYMSS82HaX5l4mpnGO7hbpPN6EWH2ltc= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= -sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= -sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s= -sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= -sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +helm.sh/helm/v3 v3.17.2 h1:agYQ5ew2jq5vdx2K7q5W44KyKQrnSubUMCQsjkiv3/o= +helm.sh/helm/v3 v3.17.2/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= +k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= +k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= +k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= +k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= +k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8= +k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc= +k8s.io/cli-runtime v0.32.3 h1:khLF2ivU2T6Q77H97atx3REY9tXiA3OLOjWJxUrdvss= +k8s.io/cli-runtime v0.32.3/go.mod h1:vZT6dZq7mZAca53rwUfdFSZjdtLyfF61mkf/8q+Xjak= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= +k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= +k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= +k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/kubectl v0.32.3 h1:VMi584rbboso+yjfv0d8uBHwwxbC438LKq+dXd5tOAI= +k8s.io/kubectl v0.32.3/go.mod h1:6Euv2aso5GKzo/UVMacV6C7miuyevpfI91SvBvV9Zdg= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= +k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go v1.2.6 h1:z8cmxQXBU8yZ4mkytWqXfo6tZcamPwjsuxYU81xJ8Lk= +oras.land/oras-go v1.2.6/go.mod h1:OVPc1PegSEe/K8YiLfosrlqlqTN9PUyFvOw5Y9gwrT8= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= From b1f5ccae58be3dde3c0c1700d2f8d0cca7105d74 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Wed, 26 Mar 2025 07:57:48 +0100 Subject: [PATCH 012/173] Add ApplyConfig utils for kubernetes lib clients Signed-off-by: Xabier Larrakoetxea --- .github/workflows/helmrelease.yaml | 2 +- CHANGELOG.md | 4 + go.mod | 2 +- .../applyconfiguration/internal/internal.go | 46 ++++ .../gen/applyconfiguration/sloth/v1/alert.go | 53 +++++ .../applyconfiguration/sloth/v1/alerting.go | 71 ++++++ .../sloth/v1/prometheusservicelevel.go | 209 ++++++++++++++++++ .../sloth/v1/prometheusservicelevelspec.go | 52 +++++ .../sloth/v1/prometheusservicelevelstatus.go | 63 ++++++ .../gen/applyconfiguration/sloth/v1/sli.go | 41 ++++ .../applyconfiguration/sloth/v1/slievents.go | 32 +++ .../applyconfiguration/sloth/v1/sliplugin.go | 38 ++++ .../gen/applyconfiguration/sloth/v1/sliraw.go | 23 ++ .../gen/applyconfiguration/sloth/v1/slo.go | 74 +++++++ .../gen/applyconfiguration/utils.go | 46 ++++ .../versioned/fake/clientset_generated.go | 33 +++ .../v1/fake/fake_prometheusservicelevel.go | 9 +- .../typed/sloth/v1/prometheusservicelevel.go | 8 +- scripts/kubegen.sh | 3 +- 19 files changed, 800 insertions(+), 9 deletions(-) create mode 100644 pkg/kubernetes/gen/applyconfiguration/internal/internal.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/alert.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/alerting.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevel.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelspec.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelstatus.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/sli.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/slievents.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/sliplugin.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/sliraw.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/slo.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/utils.go diff --git a/.github/workflows/helmrelease.yaml b/.github/workflows/helmrelease.yaml index ce15d33b..89488e05 100644 --- a/.github/workflows/helmrelease.yaml +++ b/.github/workflows/helmrelease.yaml @@ -11,7 +11,7 @@ jobs: release: runs-on: ubuntu-latest # Only on helm tags. - if: startsWith(github.ref, 'refs/tags/helm-') + if: startsWith(github.ref, 'refs/tags/v') steps: - name: Checkout uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a7e8d79..a541eac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +## Added + +- Add `ApplyConfig` utils for Kubernetes lib clients. + ### Changed - Update to Go 1.24. diff --git a/go.mod b/go.mod index a5090537..d67e5826 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( k8s.io/api v0.32.3 k8s.io/apimachinery v0.32.3 k8s.io/client-go v0.32.3 + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 ) require ( @@ -91,6 +92,5 @@ require ( sigs.k8s.io/controller-runtime v0.20.3 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/pkg/kubernetes/gen/applyconfiguration/internal/internal.go b/pkg/kubernetes/gen/applyconfiguration/internal/internal.go new file mode 100644 index 00000000..e1fd6791 --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/internal/internal.go @@ -0,0 +1,46 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package internal + +import ( + fmt "fmt" + sync "sync" + + typed "sigs.k8s.io/structured-merge-diff/v4/typed" +) + +func Parser() *typed.Parser { + parserOnce.Do(func() { + var err error + parser, err = typed.NewParser(schemaYAML) + if err != nil { + panic(fmt.Sprintf("Failed to parse schema: %v", err)) + } + }) + return parser +} + +var parserOnce sync.Once +var parser *typed.Parser +var schemaYAML = typed.YAMLObject(`types: +- name: __untyped_atomic_ + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic +- name: __untyped_deduced_ + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_deduced_ + elementRelationship: separable +`) diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/alert.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/alert.go new file mode 100644 index 00000000..2a0ae869 --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/alert.go @@ -0,0 +1,53 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// AlertApplyConfiguration represents a declarative configuration of the Alert type for use +// with apply. +type AlertApplyConfiguration struct { + Disable *bool `json:"disable,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` +} + +// AlertApplyConfiguration constructs a declarative configuration of the Alert type for use with +// apply. +func Alert() *AlertApplyConfiguration { + return &AlertApplyConfiguration{} +} + +// WithDisable sets the Disable field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Disable field is set to the value of the last call. +func (b *AlertApplyConfiguration) WithDisable(value bool) *AlertApplyConfiguration { + b.Disable = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *AlertApplyConfiguration) WithLabels(entries map[string]string) *AlertApplyConfiguration { + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *AlertApplyConfiguration) WithAnnotations(entries map[string]string) *AlertApplyConfiguration { + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/alerting.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/alerting.go new file mode 100644 index 00000000..2c6c9c6c --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/alerting.go @@ -0,0 +1,71 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// AlertingApplyConfiguration represents a declarative configuration of the Alerting type for use +// with apply. +type AlertingApplyConfiguration struct { + Name *string `json:"name,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + PageAlert *AlertApplyConfiguration `json:"pageAlert,omitempty"` + TicketAlert *AlertApplyConfiguration `json:"ticketAlert,omitempty"` +} + +// AlertingApplyConfiguration constructs a declarative configuration of the Alerting type for use with +// apply. +func Alerting() *AlertingApplyConfiguration { + return &AlertingApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *AlertingApplyConfiguration) WithName(value string) *AlertingApplyConfiguration { + b.Name = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *AlertingApplyConfiguration) WithLabels(entries map[string]string) *AlertingApplyConfiguration { + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *AlertingApplyConfiguration) WithAnnotations(entries map[string]string) *AlertingApplyConfiguration { + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} + +// WithPageAlert sets the PageAlert field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PageAlert field is set to the value of the last call. +func (b *AlertingApplyConfiguration) WithPageAlert(value *AlertApplyConfiguration) *AlertingApplyConfiguration { + b.PageAlert = value + return b +} + +// WithTicketAlert sets the TicketAlert field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TicketAlert field is set to the value of the last call. +func (b *AlertingApplyConfiguration) WithTicketAlert(value *AlertApplyConfiguration) *AlertingApplyConfiguration { + b.TicketAlert = value + return b +} diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevel.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevel.go new file mode 100644 index 00000000..f27db280 --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevel.go @@ -0,0 +1,209 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +import ( + apismetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + metav1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// PrometheusServiceLevelApplyConfiguration represents a declarative configuration of the PrometheusServiceLevel type for use +// with apply. +type PrometheusServiceLevelApplyConfiguration struct { + metav1.TypeMetaApplyConfiguration `json:",inline"` + *metav1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *PrometheusServiceLevelSpecApplyConfiguration `json:"spec,omitempty"` + Status *PrometheusServiceLevelStatusApplyConfiguration `json:"status,omitempty"` +} + +// PrometheusServiceLevel constructs a declarative configuration of the PrometheusServiceLevel type for use with +// apply. +func PrometheusServiceLevel(name, namespace string) *PrometheusServiceLevelApplyConfiguration { + b := &PrometheusServiceLevelApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("PrometheusServiceLevel") + b.WithAPIVersion("sloth.slok.dev/v1") + return b +} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithKind(value string) *PrometheusServiceLevelApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithAPIVersion(value string) *PrometheusServiceLevelApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithName(value string) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithGenerateName(value string) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithNamespace(value string) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithUID(value types.UID) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithResourceVersion(value string) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithGeneration(value int64) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithCreationTimestamp(value apismetav1.Time) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithDeletionTimestamp(value apismetav1.Time) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *PrometheusServiceLevelApplyConfiguration) WithLabels(entries map[string]string) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *PrometheusServiceLevelApplyConfiguration) WithAnnotations(entries map[string]string) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *PrometheusServiceLevelApplyConfiguration) WithOwnerReferences(values ...*metav1.OwnerReferenceApplyConfiguration) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *PrometheusServiceLevelApplyConfiguration) WithFinalizers(values ...string) *PrometheusServiceLevelApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *PrometheusServiceLevelApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &metav1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithSpec(value *PrometheusServiceLevelSpecApplyConfiguration) *PrometheusServiceLevelApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *PrometheusServiceLevelApplyConfiguration) WithStatus(value *PrometheusServiceLevelStatusApplyConfiguration) *PrometheusServiceLevelApplyConfiguration { + b.Status = value + return b +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *PrometheusServiceLevelApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelspec.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelspec.go new file mode 100644 index 00000000..8f5706a7 --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelspec.go @@ -0,0 +1,52 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// PrometheusServiceLevelSpecApplyConfiguration represents a declarative configuration of the PrometheusServiceLevelSpec type for use +// with apply. +type PrometheusServiceLevelSpecApplyConfiguration struct { + Service *string `json:"service,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + SLOs []SLOApplyConfiguration `json:"slos,omitempty"` +} + +// PrometheusServiceLevelSpecApplyConfiguration constructs a declarative configuration of the PrometheusServiceLevelSpec type for use with +// apply. +func PrometheusServiceLevelSpec() *PrometheusServiceLevelSpecApplyConfiguration { + return &PrometheusServiceLevelSpecApplyConfiguration{} +} + +// WithService sets the Service field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Service field is set to the value of the last call. +func (b *PrometheusServiceLevelSpecApplyConfiguration) WithService(value string) *PrometheusServiceLevelSpecApplyConfiguration { + b.Service = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *PrometheusServiceLevelSpecApplyConfiguration) WithLabels(entries map[string]string) *PrometheusServiceLevelSpecApplyConfiguration { + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithSLOs adds the given value to the SLOs field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the SLOs field. +func (b *PrometheusServiceLevelSpecApplyConfiguration) WithSLOs(values ...*SLOApplyConfiguration) *PrometheusServiceLevelSpecApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithSLOs") + } + b.SLOs = append(b.SLOs, *values[i]) + } + return b +} diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelstatus.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelstatus.go new file mode 100644 index 00000000..0660cca3 --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelstatus.go @@ -0,0 +1,63 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// PrometheusServiceLevelStatusApplyConfiguration represents a declarative configuration of the PrometheusServiceLevelStatus type for use +// with apply. +type PrometheusServiceLevelStatusApplyConfiguration struct { + PromOpRulesGeneratedSLOs *int `json:"promOpRulesGeneratedSLOs,omitempty"` + ProcessedSLOs *int `json:"processedSLOs,omitempty"` + PromOpRulesGenerated *bool `json:"promOpRulesGenerated,omitempty"` + LastPromOpRulesSuccessfulGenerated *metav1.Time `json:"lastPromOpRulesSuccessfulGenerated,omitempty"` + ObservedGeneration *int64 `json:"observedGeneration,omitempty"` +} + +// PrometheusServiceLevelStatusApplyConfiguration constructs a declarative configuration of the PrometheusServiceLevelStatus type for use with +// apply. +func PrometheusServiceLevelStatus() *PrometheusServiceLevelStatusApplyConfiguration { + return &PrometheusServiceLevelStatusApplyConfiguration{} +} + +// WithPromOpRulesGeneratedSLOs sets the PromOpRulesGeneratedSLOs field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PromOpRulesGeneratedSLOs field is set to the value of the last call. +func (b *PrometheusServiceLevelStatusApplyConfiguration) WithPromOpRulesGeneratedSLOs(value int) *PrometheusServiceLevelStatusApplyConfiguration { + b.PromOpRulesGeneratedSLOs = &value + return b +} + +// WithProcessedSLOs sets the ProcessedSLOs field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ProcessedSLOs field is set to the value of the last call. +func (b *PrometheusServiceLevelStatusApplyConfiguration) WithProcessedSLOs(value int) *PrometheusServiceLevelStatusApplyConfiguration { + b.ProcessedSLOs = &value + return b +} + +// WithPromOpRulesGenerated sets the PromOpRulesGenerated field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PromOpRulesGenerated field is set to the value of the last call. +func (b *PrometheusServiceLevelStatusApplyConfiguration) WithPromOpRulesGenerated(value bool) *PrometheusServiceLevelStatusApplyConfiguration { + b.PromOpRulesGenerated = &value + return b +} + +// WithLastPromOpRulesSuccessfulGenerated sets the LastPromOpRulesSuccessfulGenerated field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the LastPromOpRulesSuccessfulGenerated field is set to the value of the last call. +func (b *PrometheusServiceLevelStatusApplyConfiguration) WithLastPromOpRulesSuccessfulGenerated(value metav1.Time) *PrometheusServiceLevelStatusApplyConfiguration { + b.LastPromOpRulesSuccessfulGenerated = &value + return b +} + +// WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ObservedGeneration field is set to the value of the last call. +func (b *PrometheusServiceLevelStatusApplyConfiguration) WithObservedGeneration(value int64) *PrometheusServiceLevelStatusApplyConfiguration { + b.ObservedGeneration = &value + return b +} diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sli.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sli.go new file mode 100644 index 00000000..9e6e4063 --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sli.go @@ -0,0 +1,41 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// SLIApplyConfiguration represents a declarative configuration of the SLI type for use +// with apply. +type SLIApplyConfiguration struct { + Raw *SLIRawApplyConfiguration `json:"raw,omitempty"` + Events *SLIEventsApplyConfiguration `json:"events,omitempty"` + Plugin *SLIPluginApplyConfiguration `json:"plugin,omitempty"` +} + +// SLIApplyConfiguration constructs a declarative configuration of the SLI type for use with +// apply. +func SLI() *SLIApplyConfiguration { + return &SLIApplyConfiguration{} +} + +// WithRaw sets the Raw field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Raw field is set to the value of the last call. +func (b *SLIApplyConfiguration) WithRaw(value *SLIRawApplyConfiguration) *SLIApplyConfiguration { + b.Raw = value + return b +} + +// WithEvents sets the Events field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Events field is set to the value of the last call. +func (b *SLIApplyConfiguration) WithEvents(value *SLIEventsApplyConfiguration) *SLIApplyConfiguration { + b.Events = value + return b +} + +// WithPlugin sets the Plugin field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Plugin field is set to the value of the last call. +func (b *SLIApplyConfiguration) WithPlugin(value *SLIPluginApplyConfiguration) *SLIApplyConfiguration { + b.Plugin = value + return b +} diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/slievents.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/slievents.go new file mode 100644 index 00000000..8ccbddb5 --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/slievents.go @@ -0,0 +1,32 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// SLIEventsApplyConfiguration represents a declarative configuration of the SLIEvents type for use +// with apply. +type SLIEventsApplyConfiguration struct { + ErrorQuery *string `json:"errorQuery,omitempty"` + TotalQuery *string `json:"totalQuery,omitempty"` +} + +// SLIEventsApplyConfiguration constructs a declarative configuration of the SLIEvents type for use with +// apply. +func SLIEvents() *SLIEventsApplyConfiguration { + return &SLIEventsApplyConfiguration{} +} + +// WithErrorQuery sets the ErrorQuery field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ErrorQuery field is set to the value of the last call. +func (b *SLIEventsApplyConfiguration) WithErrorQuery(value string) *SLIEventsApplyConfiguration { + b.ErrorQuery = &value + return b +} + +// WithTotalQuery sets the TotalQuery field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TotalQuery field is set to the value of the last call. +func (b *SLIEventsApplyConfiguration) WithTotalQuery(value string) *SLIEventsApplyConfiguration { + b.TotalQuery = &value + return b +} diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sliplugin.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sliplugin.go new file mode 100644 index 00000000..2cd04981 --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sliplugin.go @@ -0,0 +1,38 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// SLIPluginApplyConfiguration represents a declarative configuration of the SLIPlugin type for use +// with apply. +type SLIPluginApplyConfiguration struct { + ID *string `json:"id,omitempty"` + Options map[string]string `json:"options,omitempty"` +} + +// SLIPluginApplyConfiguration constructs a declarative configuration of the SLIPlugin type for use with +// apply. +func SLIPlugin() *SLIPluginApplyConfiguration { + return &SLIPluginApplyConfiguration{} +} + +// WithID sets the ID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ID field is set to the value of the last call. +func (b *SLIPluginApplyConfiguration) WithID(value string) *SLIPluginApplyConfiguration { + b.ID = &value + return b +} + +// WithOptions puts the entries into the Options field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Options field, +// overwriting an existing map entries in Options field with the same key. +func (b *SLIPluginApplyConfiguration) WithOptions(entries map[string]string) *SLIPluginApplyConfiguration { + if b.Options == nil && len(entries) > 0 { + b.Options = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Options[k] = v + } + return b +} diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sliraw.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sliraw.go new file mode 100644 index 00000000..c067681d --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sliraw.go @@ -0,0 +1,23 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// SLIRawApplyConfiguration represents a declarative configuration of the SLIRaw type for use +// with apply. +type SLIRawApplyConfiguration struct { + ErrorRatioQuery *string `json:"errorRatioQuery,omitempty"` +} + +// SLIRawApplyConfiguration constructs a declarative configuration of the SLIRaw type for use with +// apply. +func SLIRaw() *SLIRawApplyConfiguration { + return &SLIRawApplyConfiguration{} +} + +// WithErrorRatioQuery sets the ErrorRatioQuery field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ErrorRatioQuery field is set to the value of the last call. +func (b *SLIRawApplyConfiguration) WithErrorRatioQuery(value string) *SLIRawApplyConfiguration { + b.ErrorRatioQuery = &value + return b +} diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/slo.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/slo.go new file mode 100644 index 00000000..fe9ee74f --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/slo.go @@ -0,0 +1,74 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// SLOApplyConfiguration represents a declarative configuration of the SLO type for use +// with apply. +type SLOApplyConfiguration struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Objective *float64 `json:"objective,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + SLI *SLIApplyConfiguration `json:"sli,omitempty"` + Alerting *AlertingApplyConfiguration `json:"alerting,omitempty"` +} + +// SLOApplyConfiguration constructs a declarative configuration of the SLO type for use with +// apply. +func SLO() *SLOApplyConfiguration { + return &SLOApplyConfiguration{} +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *SLOApplyConfiguration) WithName(value string) *SLOApplyConfiguration { + b.Name = &value + return b +} + +// WithDescription sets the Description field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Description field is set to the value of the last call. +func (b *SLOApplyConfiguration) WithDescription(value string) *SLOApplyConfiguration { + b.Description = &value + return b +} + +// WithObjective sets the Objective field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Objective field is set to the value of the last call. +func (b *SLOApplyConfiguration) WithObjective(value float64) *SLOApplyConfiguration { + b.Objective = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *SLOApplyConfiguration) WithLabels(entries map[string]string) *SLOApplyConfiguration { + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithSLI sets the SLI field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SLI field is set to the value of the last call. +func (b *SLOApplyConfiguration) WithSLI(value *SLIApplyConfiguration) *SLOApplyConfiguration { + b.SLI = value + return b +} + +// WithAlerting sets the Alerting field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Alerting field is set to the value of the last call. +func (b *SLOApplyConfiguration) WithAlerting(value *AlertingApplyConfiguration) *SLOApplyConfiguration { + b.Alerting = value + return b +} diff --git a/pkg/kubernetes/gen/applyconfiguration/utils.go b/pkg/kubernetes/gen/applyconfiguration/utils.go new file mode 100644 index 00000000..1962cfae --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/utils.go @@ -0,0 +1,46 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package applyconfiguration + +import ( + v1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" + internal "github.com/slok/sloth/pkg/kubernetes/gen/applyconfiguration/internal" + slothv1 "github.com/slok/sloth/pkg/kubernetes/gen/applyconfiguration/sloth/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + testing "k8s.io/client-go/testing" +) + +// ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no +// apply configuration type exists for the given GroupVersionKind. +func ForKind(kind schema.GroupVersionKind) interface{} { + switch kind { + // Group=sloth.slok.dev, Version=v1 + case v1.SchemeGroupVersion.WithKind("Alert"): + return &slothv1.AlertApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("Alerting"): + return &slothv1.AlertingApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("PrometheusServiceLevel"): + return &slothv1.PrometheusServiceLevelApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("PrometheusServiceLevelSpec"): + return &slothv1.PrometheusServiceLevelSpecApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("PrometheusServiceLevelStatus"): + return &slothv1.PrometheusServiceLevelStatusApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("SLI"): + return &slothv1.SLIApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("SLIEvents"): + return &slothv1.SLIEventsApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("SLIPlugin"): + return &slothv1.SLIPluginApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("SLIRaw"): + return &slothv1.SLIRawApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("SLO"): + return &slothv1.SLOApplyConfiguration{} + + } + return nil +} + +func NewTypeConverter(scheme *runtime.Scheme) *testing.TypeConverter { + return &testing.TypeConverter{Scheme: scheme, TypeResolver: internal.Parser()} +} diff --git a/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go b/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go index a39c56e4..d4f76a5a 100644 --- a/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go +++ b/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go @@ -3,6 +3,7 @@ package fake import ( + applyconfiguration "github.com/slok/sloth/pkg/kubernetes/gen/applyconfiguration" clientset "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" slothv1 "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1" fakeslothv1 "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake" @@ -62,6 +63,38 @@ func (c *Clientset) Tracker() testing.ObjectTracker { return c.tracker } +// NewClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +func NewClientset(objects ...runtime.Object) *Clientset { + o := testing.NewFieldManagedObjectTracker( + scheme, + codecs.UniversalDecoder(), + applyconfiguration.NewTypeConverter(scheme), + ) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + var ( _ clientset.Interface = &Clientset{} _ testing.FakeClient = &Clientset{} diff --git a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_prometheusservicelevel.go b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_prometheusservicelevel.go index 6e5a861d..c7e45ff7 100644 --- a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_prometheusservicelevel.go +++ b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake/fake_prometheusservicelevel.go @@ -4,19 +4,20 @@ package fake import ( v1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" - slothv1 "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1" + slothv1 "github.com/slok/sloth/pkg/kubernetes/gen/applyconfiguration/sloth/v1" + typedslothv1 "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1" gentype "k8s.io/client-go/gentype" ) // fakePrometheusServiceLevels implements PrometheusServiceLevelInterface type fakePrometheusServiceLevels struct { - *gentype.FakeClientWithList[*v1.PrometheusServiceLevel, *v1.PrometheusServiceLevelList] + *gentype.FakeClientWithListAndApply[*v1.PrometheusServiceLevel, *v1.PrometheusServiceLevelList, *slothv1.PrometheusServiceLevelApplyConfiguration] Fake *FakeSlothV1 } -func newFakePrometheusServiceLevels(fake *FakeSlothV1, namespace string) slothv1.PrometheusServiceLevelInterface { +func newFakePrometheusServiceLevels(fake *FakeSlothV1, namespace string) typedslothv1.PrometheusServiceLevelInterface { return &fakePrometheusServiceLevels{ - gentype.NewFakeClientWithList[*v1.PrometheusServiceLevel, *v1.PrometheusServiceLevelList]( + gentype.NewFakeClientWithListAndApply[*v1.PrometheusServiceLevel, *v1.PrometheusServiceLevelList, *slothv1.PrometheusServiceLevelApplyConfiguration]( fake.Fake, namespace, v1.SchemeGroupVersion.WithResource("prometheusservicelevels"), diff --git a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/prometheusservicelevel.go b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/prometheusservicelevel.go index 8fd3a334..30686c89 100644 --- a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/prometheusservicelevel.go +++ b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/prometheusservicelevel.go @@ -6,6 +6,7 @@ import ( context "context" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" + applyconfigurationslothv1 "github.com/slok/sloth/pkg/kubernetes/gen/applyconfiguration/sloth/v1" scheme "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/scheme" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" types "k8s.io/apimachinery/pkg/types" @@ -31,18 +32,21 @@ type PrometheusServiceLevelInterface interface { List(ctx context.Context, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *slothv1.PrometheusServiceLevel, err error) + Apply(ctx context.Context, prometheusServiceLevel *applyconfigurationslothv1.PrometheusServiceLevelApplyConfiguration, opts metav1.ApplyOptions) (result *slothv1.PrometheusServiceLevel, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, prometheusServiceLevel *applyconfigurationslothv1.PrometheusServiceLevelApplyConfiguration, opts metav1.ApplyOptions) (result *slothv1.PrometheusServiceLevel, err error) PrometheusServiceLevelExpansion } // prometheusServiceLevels implements PrometheusServiceLevelInterface type prometheusServiceLevels struct { - *gentype.ClientWithList[*slothv1.PrometheusServiceLevel, *slothv1.PrometheusServiceLevelList] + *gentype.ClientWithListAndApply[*slothv1.PrometheusServiceLevel, *slothv1.PrometheusServiceLevelList, *applyconfigurationslothv1.PrometheusServiceLevelApplyConfiguration] } // newPrometheusServiceLevels returns a PrometheusServiceLevels func newPrometheusServiceLevels(c *SlothV1Client, namespace string) *prometheusServiceLevels { return &prometheusServiceLevels{ - gentype.NewClientWithList[*slothv1.PrometheusServiceLevel, *slothv1.PrometheusServiceLevelList]( + gentype.NewClientWithListAndApply[*slothv1.PrometheusServiceLevel, *slothv1.PrometheusServiceLevelList, *applyconfigurationslothv1.PrometheusServiceLevelApplyConfiguration]( "prometheusservicelevels", c.RESTClient(), scheme.ParameterCodec, diff --git a/scripts/kubegen.sh b/scripts/kubegen.sh index 2aee624e..a47c3fc3 100755 --- a/scripts/kubegen.sh +++ b/scripts/kubegen.sh @@ -12,7 +12,8 @@ rm -rf ./${GEN_DIRECTORY} docker run --rm -it -v ${PWD}:/app "${IMAGE_GEN}" \ --apis-in ./pkg/kubernetes/api \ --go-gen-out ./${GEN_DIRECTORY} \ - --crd-gen-out ./${GEN_DIRECTORY}/crd + --crd-gen-out ./${GEN_DIRECTORY}/crd \ + --apply-configurations echo "Copying crd to helm chart..." rm ./deploy/kubernetes/helm/sloth/crds/* From 9e33b0a6db2194b8a7d266fc16b857fc16a7d114 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Wed, 26 Mar 2025 09:37:15 +0100 Subject: [PATCH 013/173] Update deployment manifests Git-sync from v3 to v4 Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 2 ++ deploy/kubernetes/helm/sloth/Chart.yaml | 2 +- .../helm/sloth/templates/deployment.yaml | 6 +++--- .../testdata/output/deployment_custom.yaml | 8 ++++---- .../testdata/output/deployment_default.yaml | 8 ++++---- deploy/kubernetes/helm/sloth/values.yaml | 4 ++-- .../raw/sloth-with-common-plugins.yaml | 20 +++++++++---------- deploy/kubernetes/raw/sloth.yaml | 12 +++++------ 8 files changed, 32 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a541eac1..fecbe86f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,12 @@ - Update to Go 1.24. - Update to Kubernetes v1.32. - Update all other dependencies to latest versions. +- Migrate deployment manifests `git-sync` to v4. ### Fixes - Allow spec files with CRLF. +- Helm chart tolerations ## [v0.11.0] - 2022-10-22 diff --git a/deploy/kubernetes/helm/sloth/Chart.yaml b/deploy/kubernetes/helm/sloth/Chart.yaml index 597651e7..b1a1145c 100644 --- a/deploy/kubernetes/helm/sloth/Chart.yaml +++ b/deploy/kubernetes/helm/sloth/Chart.yaml @@ -4,4 +4,4 @@ description: Base chart for Sloth. type: application home: https://github.com/slok/sloth kubeVersion: ">= 1.19.0-0" -version: 0.7.1 +version: 0.8.0 diff --git a/deploy/kubernetes/helm/sloth/templates/deployment.yaml b/deploy/kubernetes/helm/sloth/templates/deployment.yaml index 276e7fc8..c33c83fe 100644 --- a/deploy/kubernetes/helm/sloth/templates/deployment.yaml +++ b/deploy/kubernetes/helm/sloth/templates/deployment.yaml @@ -95,13 +95,13 @@ spec: image: {{ .Values.commonPlugins.image.repository }}:{{ .Values.commonPlugins.image.tag }} args: - --repo={{.Values.commonPlugins.gitRepo.url}} - - --branch={{.Values.commonPlugins.gitRepo.branch}} - - --wait=30 + - --ref={{.Values.commonPlugins.gitRepo.branch}} + - --period=30s - --webhook-url=http://localhost:8082/-/reload volumeMounts: - name: sloth-common-sli-plugins # Default path for git-sync. - mountPath: /tmp/git + mountPath: /git {{- with .Values.securityContext.container }} securityContext: {{- toYaml . | nindent 12 }} diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml index 1b7b0ff1..538270f2 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml @@ -68,16 +68,16 @@ spec: cpu: 5m memory: 75Mi - name: git-sync-plugins - image: k8s.gcr.io/git-sync/git-sync:v3.6.1 + image: registry.k8s.io/git-sync/git-sync:v4.4.0 args: - --repo=https://github.com/slok/sloth-test-common-sli-plugins - - --branch=main - - --wait=30 + - --ref=main + - --period=30s - --webhook-url=http://localhost:8082/-/reload volumeMounts: - name: sloth-common-sli-plugins # Default path for git-sync. - mountPath: /tmp/git + mountPath: /git securityContext: allowPrivilegeEscalation: false resources: diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml index 2d19c3a5..9392feb3 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml @@ -52,16 +52,16 @@ spec: cpu: 5m memory: 75Mi - name: git-sync-plugins - image: k8s.gcr.io/git-sync/git-sync:v3.6.1 + image: registry.k8s.io/git-sync/git-sync:v4.4.0 args: - --repo=https://github.com/slok/sloth-common-sli-plugins - - --branch=main - - --wait=30 + - --ref=main + - --period=30s - --webhook-url=http://localhost:8082/-/reload volumeMounts: - name: sloth-common-sli-plugins # Default path for git-sync. - mountPath: /tmp/git + mountPath: /git resources: limits: cpu: 50m diff --git a/deploy/kubernetes/helm/sloth/values.yaml b/deploy/kubernetes/helm/sloth/values.yaml index 8c8b309d..71b42f39 100644 --- a/deploy/kubernetes/helm/sloth/values.yaml +++ b/deploy/kubernetes/helm/sloth/values.yaml @@ -33,8 +33,8 @@ sloth: commonPlugins: enabled: true image: - repository: k8s.gcr.io/git-sync/git-sync - tag: v3.6.1 + repository: registry.k8s.io/git-sync/git-sync + tag: v4.4.0 gitRepo: url: https://github.com/slok/sloth-common-sli-plugins branch: main diff --git a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml index faf910bd..c40c0fab 100644 --- a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml +++ b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -105,16 +105,16 @@ spec: cpu: 5m memory: 75Mi - name: git-sync-plugins - image: k8s.gcr.io/git-sync/git-sync:v3.6.1 + image: registry.k8s.io/git-sync/git-sync:v4.4.0 args: - --repo=https://github.com/slok/sloth-common-sli-plugins - - --branch=main - - --wait=30 + - --ref=main + - --period=30s - --webhook-url=http://localhost:8082/-/reload volumeMounts: - name: sloth-common-sli-plugins # Default path for git-sync. - mountPath: /tmp/git + mountPath: /git resources: limits: cpu: 50m @@ -133,7 +133,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth diff --git a/deploy/kubernetes/raw/sloth.yaml b/deploy/kubernetes/raw/sloth.yaml index edca04bc..727dd70a 100644 --- a/deploy/kubernetes/raw/sloth.yaml +++ b/deploy/kubernetes/raw/sloth.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -108,7 +108,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.7.0 + helm.sh/chart: sloth-0.8.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth From b7338c2e2dc343274c8d641e0198a6735de4aa94 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Thu, 27 Mar 2025 07:37:03 +0100 Subject: [PATCH 014/173] Prepare v0.12.0 release Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 5 ++++- deploy/kubernetes/helm/sloth/Chart.yaml | 2 +- .../tests/testdata/output/deployment_default.yaml | 2 +- deploy/kubernetes/helm/sloth/values.yaml | 2 +- .../kubernetes/raw/sloth-with-common-plugins.yaml | 14 +++++++------- deploy/kubernetes/raw/sloth.yaml | 14 +++++++------- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fecbe86f..fb34c616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [v0.12.0] - 2025-03-27 + ## Added - Add `ApplyConfig` utils for Kubernetes lib clients. @@ -178,7 +180,8 @@ - Support raw query based SLI. - Kubernetes (prometheus-operator) CRD generation support. -[unreleased]: https://github.com/slok/sloth/compare/v0.11.0...HEAD +[unreleased]: https://github.com/slok/sloth/compare/v0.12.0...HEAD +[v0.12.0]: https://github.com/slok/sloth/compare/v0.11.0...v0.12.0 [v0.11.0]: https://github.com/slok/sloth/compare/v0.10.0...v0.11.0 [v0.10.0]: https://github.com/slok/sloth/compare/v0.9.0...v0.10.0 [v0.9.0]: https://github.com/slok/sloth/compare/v0.8.0...v0.9.0 diff --git a/deploy/kubernetes/helm/sloth/Chart.yaml b/deploy/kubernetes/helm/sloth/Chart.yaml index b1a1145c..c943507e 100644 --- a/deploy/kubernetes/helm/sloth/Chart.yaml +++ b/deploy/kubernetes/helm/sloth/Chart.yaml @@ -4,4 +4,4 @@ description: Base chart for Sloth. type: application home: https://github.com/slok/sloth kubeVersion: ">= 1.19.0-0" -version: 0.8.0 +version: 0.12.0 diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml index 9392feb3..40805140 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml @@ -32,7 +32,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.11.0 + image: ghcr.io/slok/sloth:v0.12.0 args: - kubernetes-controller - --sli-plugins-path=/plugins diff --git a/deploy/kubernetes/helm/sloth/values.yaml b/deploy/kubernetes/helm/sloth/values.yaml index 71b42f39..3c6810c9 100644 --- a/deploy/kubernetes/helm/sloth/values.yaml +++ b/deploy/kubernetes/helm/sloth/values.yaml @@ -2,7 +2,7 @@ labels: {} image: repository: ghcr.io/slok/sloth - tag: v0.11.0 + tag: v0.12.0 # -- Container resources: requests and limits for CPU, Memory resources: diff --git a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml index c40c0fab..e5950445 100644 --- a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml +++ b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -85,7 +85,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.11.0 + image: ghcr.io/slok/sloth:v0.12.0 args: - kubernetes-controller - --sli-plugins-path=/plugins @@ -133,7 +133,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth diff --git a/deploy/kubernetes/raw/sloth.yaml b/deploy/kubernetes/raw/sloth.yaml index 727dd70a..3a820a28 100644 --- a/deploy/kubernetes/raw/sloth.yaml +++ b/deploy/kubernetes/raw/sloth.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -85,7 +85,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.11.0 + image: ghcr.io/slok/sloth:v0.12.0 args: - kubernetes-controller - --logger=default @@ -108,7 +108,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.8.0 + helm.sh/chart: sloth-0.12.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth From bbdd95e80674fb137545d00e7f1967b11e9a5a18 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Thu, 27 Mar 2025 08:07:15 +0100 Subject: [PATCH 015/173] Fix binary build script Signed-off-by: Xabier Larrakoetxea --- scripts/build/bin/build-raw.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/bin/build-raw.sh b/scripts/build/bin/build-raw.sh index eb13684b..1670fa86 100755 --- a/scripts/build/bin/build-raw.sh +++ b/scripts/build/bin/build-raw.sh @@ -21,4 +21,4 @@ f_ver="-X ${version_path}=${VERSION:-dev}" # Build binary. echo "[*] Building binary at ${final_out} (GOOS=${GOOS:-}, GOARCH=${GOARCH:-}, GOARM=${GOARM:-}, VERSION=${VERSION:-}, EXTENSION=${EXTENSION:-})" -CGO_ENABLED=0 go build -o ${final_out} --ldflags "${ldf_cmp} ${f_ver}" ${src} +CGO_ENABLED=0 go build -o ${final_out} --ldflags "${ldf_cmp} ${f_ver}" -buildvcs=false ${src} From 0b36bb8d21909ec5e459a8dc0c2f7ab9b6f02097 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Thu, 27 Mar 2025 08:24:30 +0100 Subject: [PATCH 016/173] Prepare deployoment manifests release for v0.12.0 Signed-off-by: Xabier Larrakoetxea --- .github/workflows/helmrelease.yaml | 4 +--- deploy/kubernetes/helm/sloth/Chart.yaml | 2 +- deploy/kubernetes/raw/sloth-with-common-plugins.yaml | 12 ++++++------ deploy/kubernetes/raw/sloth.yaml | 12 ++++++------ 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/.github/workflows/helmrelease.yaml b/.github/workflows/helmrelease.yaml index 89488e05..da58ffd8 100644 --- a/.github/workflows/helmrelease.yaml +++ b/.github/workflows/helmrelease.yaml @@ -5,13 +5,11 @@ on: branches: - main paths: - - "deploy/kubernetes/helm/**" + - "deploy/kubernetes/helm/sloth/Chart.yaml" jobs: release: runs-on: ubuntu-latest - # Only on helm tags. - if: startsWith(github.ref, 'refs/tags/v') steps: - name: Checkout uses: actions/checkout@v4 diff --git a/deploy/kubernetes/helm/sloth/Chart.yaml b/deploy/kubernetes/helm/sloth/Chart.yaml index c943507e..7a1afe0a 100644 --- a/deploy/kubernetes/helm/sloth/Chart.yaml +++ b/deploy/kubernetes/helm/sloth/Chart.yaml @@ -4,4 +4,4 @@ description: Base chart for Sloth. type: application home: https://github.com/slok/sloth kubeVersion: ">= 1.19.0-0" -version: 0.12.0 +version: 0.12.1 diff --git a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml index e5950445..3c56d2ce 100644 --- a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml +++ b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -133,7 +133,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth diff --git a/deploy/kubernetes/raw/sloth.yaml b/deploy/kubernetes/raw/sloth.yaml index 3a820a28..f73316b1 100644 --- a/deploy/kubernetes/raw/sloth.yaml +++ b/deploy/kubernetes/raw/sloth.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -108,7 +108,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.0 + helm.sh/chart: sloth-0.12.1 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth From 1ee76f4d0bd4b9fe9682b50fa4594e5884b046ac Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Thu, 27 Mar 2025 09:09:00 +0100 Subject: [PATCH 017/173] Add Stale CI to autoclose issues and PRs Signed-off-by: Xabier Larrakoetxea --- .github/workflows/close-stale.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/close-stale.yaml diff --git a/.github/workflows/close-stale.yaml b/.github/workflows/close-stale.yaml new file mode 100644 index 00000000..fd98131b --- /dev/null +++ b/.github/workflows/close-stale.yaml @@ -0,0 +1,22 @@ +name: "Close stale issues and PRs" +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 60 + days-before-close: 15 + stale-issue-message: "This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 15 days." + close-issue-message: "This issue was closed because it has been stale for 15 days with no activity." + stale-pr-message: "This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 15 days." + close-pr-message: "This PR was closed because it has been stale for 15 days with no activity." + stale-issue-label: stale + stale-pr-label: stale + exempt-issue-labels: no-stale + exempt-pr-labels: no-stale + exempt-draft-pr: true From da8c4ac9df8f38613097b0a8b8528c8c9ece0e3d Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 29 Mar 2025 08:34:24 +0100 Subject: [PATCH 018/173] Start moving models to an importable own package Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 5 + cmd/sloth/commands/generate.go | 15 +- internal/alert/alert.go | 63 +--- internal/alert/alert_test.go | 57 ++-- internal/app/generate/noop.go | 10 +- internal/app/generate/prometheus.go | 16 +- internal/app/generate/prometheus_test.go | 24 +- internal/app/kubecontroller/handler.go | 5 +- internal/info/info.go | 17 - internal/k8sprometheus/model_test.go | 2 +- internal/prometheus/alert_rules.go | 27 +- internal/prometheus/alert_rules_test.go | 24 +- internal/prometheus/conventions.go | 17 - internal/prometheus/helpers.go | 5 +- internal/prometheus/model.go | 291 ++---------------- internal/prometheus/recording_rules.go | 70 ++--- internal/prometheus/recording_rules_test.go | 37 ++- pkg/common/conventions/conventions.go | 20 ++ pkg/common/model/alert.go | 46 +++ pkg/common/model/info.go | 18 ++ pkg/common/model/slo_prometheus.go | 274 +++++++++++++++++ .../common/model/slo_prometheus_test.go | 188 +++++------ 22 files changed, 632 insertions(+), 599 deletions(-) delete mode 100644 internal/prometheus/conventions.go create mode 100644 pkg/common/conventions/conventions.go create mode 100644 pkg/common/model/alert.go create mode 100644 pkg/common/model/info.go create mode 100644 pkg/common/model/slo_prometheus.go rename internal/prometheus/model_test.go => pkg/common/model/slo_prometheus_test.go (53%) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb34c616..42c47349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Added + +- Sloth domain models can be imported in Go apps using `github.com/slok/sloth/pkg/common/model`. +- Sloth conventions can be imported in Go apps using `github.com/slok/sloth/pkg/common/conventions`. + ## [v0.12.0] - 2025-03-27 ## Added diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 2b2f85bf..5388deb6 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -23,6 +23,7 @@ import ( "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/openslo" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/pkg/common/model" kubernetesv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" prometheusv1 "github.com/slok/sloth/pkg/prometheus/api/v1" ) @@ -310,9 +311,9 @@ type generator struct { // GeneratePrometheus generates the SLOs based on a raw regular Prometheus spec format input and outs a Prometheus raw yaml. func (g generator) GeneratePrometheus(ctx context.Context, slos prometheus.SLOGroup, out io.Writer) error { g.logger.Infof("Generating from Prometheus spec") - info := info.Info{ + info := model.Info{ Version: info.Version, - Mode: info.ModeCLIGenPrometheus, + Mode: model.ModeCLIGenPrometheus, Spec: prometheusv1.Version, } @@ -342,9 +343,9 @@ func (g generator) GeneratePrometheus(ctx context.Context, slos prometheus.SLOGr func (g generator) GenerateKubernetes(ctx context.Context, sloGroup k8sprometheus.SLOGroup, out io.Writer) error { g.logger.Infof("Generating from Kubernetes Prometheus spec") - info := info.Info{ + info := model.Info{ Version: info.Version, - Mode: info.ModeCLIGenKubernetes, + Mode: model.ModeCLIGenKubernetes, Spec: fmt.Sprintf("%s/%s", kubernetesv1.SchemeGroupVersion.Group, kubernetesv1.SchemeGroupVersion.Version), } result, err := g.generateRules(ctx, info, sloGroup.SLOGroup) @@ -372,9 +373,9 @@ func (g generator) GenerateKubernetes(ctx context.Context, sloGroup k8sprometheu // generateOpenSLO generates the SLOs based on a OpenSLO spec format input and outs a Prometheus raw yaml. func (g generator) GenerateOpenSLO(ctx context.Context, slos prometheus.SLOGroup, out io.Writer) error { g.logger.Infof("Generating from OpenSLO spec") - info := info.Info{ + info := model.Info{ Version: info.Version, - Mode: info.ModeCLIGenOpenSLO, + Mode: model.ModeCLIGenOpenSLO, Spec: openslov1alpha.APIVersion, } @@ -401,7 +402,7 @@ func (g generator) GenerateOpenSLO(ctx context.Context, slos prometheus.SLOGroup } // generate is the main generator logic that all the spec types and storers share. Mainly has the logic of the generate app service. -func (g generator) generateRules(ctx context.Context, info info.Info, slos prometheus.SLOGroup) (*generate.Response, error) { +func (g generator) generateRules(ctx context.Context, info model.Info, slos prometheus.SLOGroup) (*generate.Response, error) { // Disable recording rules if required. var sliRuleGen generate.SLIRecordingRulesGenerator = generate.NoopSLIRecordingRulesGenerator var metaRuleGen generate.MetadataRecordingRulesGenerator = generate.NoopMetadataRecordingRulesGenerator diff --git a/internal/alert/alert.go b/internal/alert/alert.go index 5dfc3eda..04d46338 100644 --- a/internal/alert/alert.go +++ b/internal/alert/alert.go @@ -4,51 +4,10 @@ import ( "context" "fmt" "time" -) - -// Severity is the type of alert. -type Severity int -const ( - UnknownAlertSeverity Severity = iota - PageAlertSeverity - TicketAlertSeverity + "github.com/slok/sloth/pkg/common/model" ) -func (s Severity) String() string { - switch s { - case PageAlertSeverity: - return "page" - case TicketAlertSeverity: - return "ticket" - default: - return "unknown" - } -} - -// MWMBAlert represents a multiwindow, multi-burn rate alert. -type MWMBAlert struct { - ID string - ShortWindow time.Duration - LongWindow time.Duration - BurnRateFactor float64 - ErrorBudget float64 - Severity Severity -} - -// MWMBAlertGroup what represents all the alerts of an SLO. -// ITs divided into two groups that are made of 2 alerts: -// - Page & quick: Critical alerts that trigger in high rate burn in short term. -// - Page & slow: Critical alerts that trigger in high-normal rate burn in medium term. -// - Ticket & slow: Warning alerts that trigger in normal rate burn in medium term. -// - Ticket & slow: Warning alerts that trigger in slow rate burn in long term. -type MWMBAlertGroup struct { - PageQuick MWMBAlert - PageSlow MWMBAlert - TicketQuick MWMBAlert - TicketSlow MWMBAlert -} - // WindowsRepo knows how to retrieve windows based on the period of time. type WindowsRepo interface { GetWindows(ctx context.Context, period time.Duration) (*Windows, error) @@ -72,7 +31,7 @@ type SLO struct { Objective float64 } -func (g Generator) GenerateMWMBAlerts(ctx context.Context, slo SLO) (*MWMBAlertGroup, error) { +func (g Generator) GenerateMWMBAlerts(ctx context.Context, slo SLO) (*model.MWMBAlertGroup, error) { windows, err := g.windowsRepo.GetWindows(ctx, slo.TimeWindow) if err != nil { return nil, fmt.Errorf("the %s SLO period time window is not supported", slo.TimeWindow) @@ -80,38 +39,38 @@ func (g Generator) GenerateMWMBAlerts(ctx context.Context, slo SLO) (*MWMBAlertG errorBudget := 100 - slo.Objective - group := MWMBAlertGroup{ - PageQuick: MWMBAlert{ + group := model.MWMBAlertGroup{ + PageQuick: model.MWMBAlert{ ID: fmt.Sprintf("%s-page-quick", slo.ID), ShortWindow: windows.PageQuick.ShortWindow, LongWindow: windows.PageQuick.LongWindow, BurnRateFactor: windows.GetSpeedPageQuick(), ErrorBudget: errorBudget, - Severity: PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - PageSlow: MWMBAlert{ + PageSlow: model.MWMBAlert{ ID: fmt.Sprintf("%s-page-slow", slo.ID), ShortWindow: windows.PageSlow.ShortWindow, LongWindow: windows.PageSlow.LongWindow, BurnRateFactor: windows.GetSpeedPageSlow(), ErrorBudget: errorBudget, - Severity: PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - TicketQuick: MWMBAlert{ + TicketQuick: model.MWMBAlert{ ID: fmt.Sprintf("%s-ticket-quick", slo.ID), ShortWindow: windows.TicketQuick.ShortWindow, LongWindow: windows.TicketQuick.LongWindow, BurnRateFactor: windows.GetSpeedTicketQuick(), ErrorBudget: errorBudget, - Severity: TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, - TicketSlow: MWMBAlert{ + TicketSlow: model.MWMBAlert{ ID: fmt.Sprintf("%s-ticket-slow", slo.ID), ShortWindow: windows.TicketSlow.ShortWindow, LongWindow: windows.TicketSlow.LongWindow, BurnRateFactor: windows.GetSpeedTicketSlow(), ErrorBudget: errorBudget, - Severity: TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, } diff --git a/internal/alert/alert_test.go b/internal/alert/alert_test.go index 977e4df4..d090998d 100644 --- a/internal/alert/alert_test.go +++ b/internal/alert/alert_test.go @@ -11,13 +11,14 @@ import ( "github.com/stretchr/testify/require" "github.com/slok/sloth/internal/alert" + "github.com/slok/sloth/pkg/common/model" ) func TestGenerateMWMBAlerts(t *testing.T) { tests := map[string]struct { windowsFS func() fs.FS slo alert.SLO - expAlerts *alert.MWMBAlertGroup + expAlerts *model.MWMBAlertGroup expErr bool }{ "Generating alerts with not supported time windows should fail.": { @@ -37,39 +38,39 @@ func TestGenerateMWMBAlerts(t *testing.T) { TimeWindow: 30 * 24 * time.Hour, Objective: 99.9, }, - expAlerts: &alert.MWMBAlertGroup{ - PageQuick: alert.MWMBAlert{ + expAlerts: &model.MWMBAlertGroup{ + PageQuick: model.MWMBAlert{ ID: "test-page-quick", ShortWindow: 5 * time.Minute, LongWindow: 1 * time.Hour, BurnRateFactor: 14.4, ErrorBudget: 0.09999999999999432, - Severity: alert.PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - PageSlow: alert.MWMBAlert{ + PageSlow: model.MWMBAlert{ ID: "test-page-slow", ShortWindow: 30 * time.Minute, LongWindow: 6 * time.Hour, BurnRateFactor: 6, ErrorBudget: 0.09999999999999432, - Severity: alert.PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - TicketQuick: alert.MWMBAlert{ + TicketQuick: model.MWMBAlert{ ID: "test-ticket-quick", ShortWindow: 2 * time.Hour, LongWindow: 1 * 24 * time.Hour, BurnRateFactor: 3, ErrorBudget: 0.09999999999999432, - Severity: alert.TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, - TicketSlow: alert.MWMBAlert{ + TicketSlow: model.MWMBAlert{ ID: "test-ticket-slow", ShortWindow: 6 * time.Hour, LongWindow: 3 * 24 * time.Hour, BurnRateFactor: 1, ErrorBudget: 0.09999999999999432, - Severity: alert.TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, }, }, @@ -81,39 +82,39 @@ func TestGenerateMWMBAlerts(t *testing.T) { TimeWindow: 28 * 24 * time.Hour, Objective: 99.9, }, - expAlerts: &alert.MWMBAlertGroup{ - PageQuick: alert.MWMBAlert{ + expAlerts: &model.MWMBAlertGroup{ + PageQuick: model.MWMBAlert{ ID: "test-page-quick", ShortWindow: 5 * time.Minute, LongWindow: 1 * time.Hour, BurnRateFactor: 13.44, ErrorBudget: 0.09999999999999432, - Severity: alert.PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - PageSlow: alert.MWMBAlert{ + PageSlow: model.MWMBAlert{ ID: "test-page-slow", ShortWindow: 30 * time.Minute, LongWindow: 6 * time.Hour, BurnRateFactor: 5.6000000000000005, ErrorBudget: 0.09999999999999432, - Severity: alert.PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - TicketQuick: alert.MWMBAlert{ + TicketQuick: model.MWMBAlert{ ID: "test-ticket-quick", ShortWindow: 2 * time.Hour, LongWindow: 1 * 24 * time.Hour, BurnRateFactor: 2.8000000000000003, ErrorBudget: 0.09999999999999432, - Severity: alert.TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, - TicketSlow: alert.MWMBAlert{ + TicketSlow: model.MWMBAlert{ ID: "test-ticket-slow", ShortWindow: 6 * time.Hour, LongWindow: 3 * 24 * time.Hour, BurnRateFactor: 0.9333333333333333, ErrorBudget: 0.09999999999999432, - Severity: alert.TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, }, }, @@ -174,39 +175,39 @@ spec: Objective: 99.9, }, - expAlerts: &alert.MWMBAlertGroup{ - PageQuick: alert.MWMBAlert{ + expAlerts: &model.MWMBAlertGroup{ + PageQuick: model.MWMBAlert{ ID: "test-page-quick", ShortWindow: 5 * time.Minute, LongWindow: 1 * time.Hour, BurnRateFactor: 13.44, ErrorBudget: 0.09999999999999432, - Severity: alert.PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - PageSlow: alert.MWMBAlert{ + PageSlow: model.MWMBAlert{ ID: "test-page-slow", ShortWindow: 30 * time.Minute, LongWindow: 6 * time.Hour, BurnRateFactor: 3.5, ErrorBudget: 0.09999999999999432, - Severity: alert.PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - TicketQuick: alert.MWMBAlert{ + TicketQuick: model.MWMBAlert{ ID: "test-ticket-quick", ShortWindow: 2 * time.Hour, LongWindow: 1 * 24 * time.Hour, BurnRateFactor: 1.4000000000000001, ErrorBudget: 0.09999999999999432, - Severity: alert.TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, - TicketSlow: alert.MWMBAlert{ + TicketSlow: model.MWMBAlert{ ID: "test-ticket-slow", ShortWindow: 6 * time.Hour, LongWindow: 3 * 24 * time.Hour, BurnRateFactor: 0.98, ErrorBudget: 0.09999999999999432, - Severity: alert.TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, }, }, diff --git a/internal/app/generate/noop.go b/internal/app/generate/noop.go index c0ee12dd..a9d90c39 100644 --- a/internal/app/generate/noop.go +++ b/internal/app/generate/noop.go @@ -4,16 +4,14 @@ import ( "context" "github.com/prometheus/prometheus/model/rulefmt" - "github.com/slok/sloth/internal/alert" - "github.com/slok/sloth/internal/info" - "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/pkg/common/model" ) type noopSLIRecordingRulesGenerator bool const NoopSLIRecordingRulesGenerator = noopSLIRecordingRulesGenerator(false) -func (noopSLIRecordingRulesGenerator) GenerateSLIRecordingRules(ctx context.Context, slo prometheus.SLO, alerts alert.MWMBAlertGroup) ([]rulefmt.Rule, error) { +func (noopSLIRecordingRulesGenerator) GenerateSLIRecordingRules(ctx context.Context, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { return nil, nil } @@ -21,7 +19,7 @@ type noopMetadataRecordingRulesGenerator bool const NoopMetadataRecordingRulesGenerator = noopMetadataRecordingRulesGenerator(false) -func (noopMetadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx context.Context, info info.Info, slo prometheus.SLO, alerts alert.MWMBAlertGroup) ([]rulefmt.Rule, error) { +func (noopMetadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx context.Context, info model.Info, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { return nil, nil } @@ -29,6 +27,6 @@ type noopSLOAlertRulesGenerator bool const NoopSLOAlertRulesGenerator = noopSLOAlertRulesGenerator(false) -func (noopSLOAlertRulesGenerator) GenerateSLOAlertRules(ctx context.Context, slo prometheus.SLO, alerts alert.MWMBAlertGroup) ([]rulefmt.Rule, error) { +func (noopSLOAlertRulesGenerator) GenerateSLOAlertRules(ctx context.Context, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { return nil, nil } diff --git a/internal/app/generate/prometheus.go b/internal/app/generate/prometheus.go index 7bf2c2cf..bb677f44 100644 --- a/internal/app/generate/prometheus.go +++ b/internal/app/generate/prometheus.go @@ -7,9 +7,9 @@ import ( "github.com/prometheus/prometheus/model/rulefmt" "github.com/slok/sloth/internal/alert" - "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/pkg/common/model" ) // ServiceConfig is the application service configuration. @@ -48,22 +48,22 @@ func (c *ServiceConfig) defaults() error { // AlertGenerator knows how to generate multiwindow multi-burn SLO alerts. type AlertGenerator interface { - GenerateMWMBAlerts(ctx context.Context, slo alert.SLO) (*alert.MWMBAlertGroup, error) + GenerateMWMBAlerts(ctx context.Context, slo alert.SLO) (*model.MWMBAlertGroup, error) } // SLIRecordingRulesGenerator knows how to generate SLI recording rules. type SLIRecordingRulesGenerator interface { - GenerateSLIRecordingRules(ctx context.Context, slo prometheus.SLO, alerts alert.MWMBAlertGroup) ([]rulefmt.Rule, error) + GenerateSLIRecordingRules(ctx context.Context, slo prometheus.SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) } // MetadataRecordingRulesGenerator knows how to generate metadata recording rules. type MetadataRecordingRulesGenerator interface { - GenerateMetadataRecordingRules(ctx context.Context, info info.Info, slo prometheus.SLO, alerts alert.MWMBAlertGroup) ([]rulefmt.Rule, error) + GenerateMetadataRecordingRules(ctx context.Context, info model.Info, slo prometheus.SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) } // SLOAlertRulesGenerator knows hot to generate SLO alert rules. type SLOAlertRulesGenerator interface { - GenerateSLOAlertRules(ctx context.Context, slo prometheus.SLO, alerts alert.MWMBAlertGroup) ([]rulefmt.Rule, error) + GenerateSLOAlertRules(ctx context.Context, slo prometheus.SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) } // Service is the application service for the generation of SLO for Prometheus. @@ -93,7 +93,7 @@ func NewService(config ServiceConfig) (*Service, error) { type Request struct { // Info about the application and execution, normally used as metadata. - Info info.Info + Info model.Info // ExtraLabels are the extra labels added to the SLOs on execution time. ExtraLabels map[string]string // SLOGroup are the SLOs group that will be used to generate the SLO results and Prom rules. @@ -102,7 +102,7 @@ type Request struct { type SLOResult struct { SLO prometheus.SLO - Alerts alert.MWMBAlertGroup + Alerts model.MWMBAlertGroup SLORules prometheus.SLORules } @@ -136,7 +136,7 @@ func (s Service) Generate(ctx context.Context, r Request) (*Response, error) { }, nil } -func (s Service) generateSLO(ctx context.Context, info info.Info, slo prometheus.SLO) (*SLOResult, error) { +func (s Service) generateSLO(ctx context.Context, info model.Info, slo prometheus.SLO) (*SLOResult, error) { logger := s.logger.WithCtxValues(ctx).WithValues(log.Kv{"slo": slo.ID}) // Generate the MWMB alerts. diff --git a/internal/app/generate/prometheus_test.go b/internal/app/generate/prometheus_test.go index 8a31a4ed..0d72e66e 100644 --- a/internal/app/generate/prometheus_test.go +++ b/internal/app/generate/prometheus_test.go @@ -11,8 +11,8 @@ import ( "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/app/generate" - "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/pkg/common/model" ) func TestIntegrationAppServiceGenerate(t *testing.T) { @@ -32,9 +32,9 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { "extra_k1": "extra_v1", "extra_k2": "extra_v2", }, - Info: info.Info{ + Info: model.Info{ Version: "test-ver", - Mode: info.ModeTest, + Mode: model.ModeTest, Spec: "test-spec", }, SLOGroup: prometheus.SLOGroup{SLOs: []prometheus.SLO{ @@ -96,39 +96,39 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { Annotations: map[string]string{"t_alert_annot": "t_label_an_1"}, }, }, - Alerts: alert.MWMBAlertGroup{ - PageQuick: alert.MWMBAlert{ + Alerts: model.MWMBAlertGroup{ + PageQuick: model.MWMBAlert{ ID: "test-id-page-quick", ShortWindow: 5 * time.Minute, LongWindow: 1 * time.Hour, BurnRateFactor: 14.4, ErrorBudget: 0.09999999999999432, - Severity: alert.PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - PageSlow: alert.MWMBAlert{ + PageSlow: model.MWMBAlert{ ID: "test-id-page-slow", ShortWindow: 30 * time.Minute, LongWindow: 6 * time.Hour, BurnRateFactor: 6, ErrorBudget: 0.09999999999999432, - Severity: alert.PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - TicketQuick: alert.MWMBAlert{ + TicketQuick: model.MWMBAlert{ ID: "test-id-ticket-quick", ShortWindow: 2 * time.Hour, LongWindow: 1 * 24 * time.Hour, BurnRateFactor: 3, ErrorBudget: 0.09999999999999432, - Severity: alert.TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, - TicketSlow: alert.MWMBAlert{ + TicketSlow: model.MWMBAlert{ ID: "test-id-ticket-slow", ShortWindow: 6 * time.Hour, LongWindow: 3 * 24 * time.Hour, BurnRateFactor: 1, ErrorBudget: 0.09999999999999432, - Severity: alert.TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, }, SLORules: prometheus.SLORules{ diff --git a/internal/app/kubecontroller/handler.go b/internal/app/kubecontroller/handler.go index eeb77151..c9afed09 100644 --- a/internal/app/kubecontroller/handler.go +++ b/internal/app/kubecontroller/handler.go @@ -12,6 +12,7 @@ import ( "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/k8sprometheus" "github.com/slok/sloth/internal/log" + commonmodel "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" ) @@ -146,9 +147,9 @@ func (h handler) handlePrometheusServiceLevelV1(ctx context.Context, psl *slothv // Generate rules. req := generate.Request{ - Info: info.Info{ + Info: commonmodel.Info{ Version: info.Version, - Mode: info.ModeControllerGenKubernetes, + Mode: commonmodel.ModeControllerGenKubernetes, Spec: fmt.Sprintf("%s/%s", slothv1.SchemeGroupVersion.Group, slothv1.SchemeGroupVersion.Version), }, ExtraLabels: h.extraLabels, diff --git a/internal/info/info.go b/internal/info/info.go index 53e9524c..eed0efde 100644 --- a/internal/info/info.go +++ b/internal/info/info.go @@ -4,20 +4,3 @@ var ( // Version is the version app. Version = "dev" ) - -type Mode string - -const ( - ModeTest = "test" - ModeCLIGenPrometheus = "cli-gen-prom" - ModeCLIGenKubernetes = "cli-gen-k8s" - ModeCLIGenOpenSLO = "cli-gen-openslo" - ModeControllerGenKubernetes = "ctrl-gen-k8s" -) - -// Info is the information of the app and request based for SLO generators. -type Info struct { - Version string - Mode Mode - Spec string -} diff --git a/internal/k8sprometheus/model_test.go b/internal/k8sprometheus/model_test.go index 25686e22..dd9b87e2 100644 --- a/internal/k8sprometheus/model_test.go +++ b/internal/k8sprometheus/model_test.go @@ -115,7 +115,7 @@ func TestModelValidationSpec(t *testing.T) { sg.SLOs[0].ID = "" return sg }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'required' tag", }, } diff --git a/internal/prometheus/alert_rules.go b/internal/prometheus/alert_rules.go index f402d520..5e53db82 100644 --- a/internal/prometheus/alert_rules.go +++ b/internal/prometheus/alert_rules.go @@ -8,11 +8,12 @@ import ( "github.com/prometheus/prometheus/model/rulefmt" - "github.com/slok/sloth/internal/alert" + "github.com/slok/sloth/pkg/common/conventions" + "github.com/slok/sloth/pkg/common/model" ) // genFunc knows how to generate an SLI recording rule for a specific time window. -type alertGenFunc func(slo SLO, sloAlert AlertMeta, quick, slow alert.MWMBAlert) (*rulefmt.Rule, error) +type alertGenFunc func(slo SLO, sloAlert AlertMeta, quick, slow model.MWMBAlert) (*rulefmt.Rule, error) type sloAlertRulesGenerator struct { alertGenFunc alertGenFunc @@ -22,7 +23,7 @@ type sloAlertRulesGenerator struct { // from an SLO. var SLOAlertRulesGenerator = sloAlertRulesGenerator{alertGenFunc: defaultSLOAlertGenerator} -func (s sloAlertRulesGenerator) GenerateSLOAlertRules(ctx context.Context, slo SLO, alerts alert.MWMBAlertGroup) ([]rulefmt.Rule, error) { +func (s sloAlertRulesGenerator) GenerateSLOAlertRules(ctx context.Context, slo SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { rules := []rulefmt.Rule{} // Generate Page alerts. @@ -48,9 +49,9 @@ func (s sloAlertRulesGenerator) GenerateSLOAlertRules(ctx context.Context, slo S return rules, nil } -func defaultSLOAlertGenerator(slo SLO, sloAlert AlertMeta, quick, slow alert.MWMBAlert) (*rulefmt.Rule, error) { +func defaultSLOAlertGenerator(slo SLO, sloAlert AlertMeta, quick, slow model.MWMBAlert) (*rulefmt.Rule, error) { // Generate the filter labels based on the SLO ids. - metricFilter := labelsToPromFilter(slo.GetSLOIDPromLabels()) + metricFilter := labelsToPromFilter(getSLOIDPromLabels(slo)) // Render the alert template. tplData := struct { @@ -68,15 +69,15 @@ func defaultSLOAlertGenerator(slo SLO, sloAlert AlertMeta, quick, slow alert.MWM }{ MetricFilter: metricFilter, ErrorBudgetRatio: quick.ErrorBudget / 100, // Any(quick or slow) should work because are the same. - QuickShortMetric: slo.GetSLIErrorMetric(quick.ShortWindow), + QuickShortMetric: getSLIErrorMetric(quick.ShortWindow), QuickShortBurnFactor: quick.BurnRateFactor, - QuickLongMetric: slo.GetSLIErrorMetric(quick.LongWindow), + QuickLongMetric: getSLIErrorMetric(quick.LongWindow), QuickLongBurnFactor: quick.BurnRateFactor, - SlowShortMetric: slo.GetSLIErrorMetric(slow.ShortWindow), + SlowShortMetric: getSLIErrorMetric(slow.ShortWindow), SlowShortBurnFactor: slow.BurnRateFactor, - SlowQuickMetric: slo.GetSLIErrorMetric(slow.LongWindow), + SlowQuickMetric: getSLIErrorMetric(slow.LongWindow), SlowQuickBurnFactor: slow.BurnRateFactor, - WindowLabel: sloWindowLabelName, + WindowLabel: conventions.PromSLOWindowLabelName, } var expr bytes.Buffer err := mwmbAlertTpl.Execute(&expr, tplData) @@ -87,14 +88,14 @@ func defaultSLOAlertGenerator(slo SLO, sloAlert AlertMeta, quick, slow alert.MWM // Add specific annotations. severity := quick.Severity.String() // Any(quick or slow) should work because are the same. extraAnnotations := map[string]string{ - "title": fmt.Sprintf("(%s) {{$labels.%s}} {{$labels.%s}} SLO error budget burn rate is too fast.", severity, sloServiceLabelName, sloNameLabelName), - "summary": fmt.Sprintf("{{$labels.%s}} {{$labels.%s}} SLO error budget burn rate is over expected.", sloServiceLabelName, sloNameLabelName), + "title": fmt.Sprintf("(%s) {{$labels.%s}} {{$labels.%s}} SLO error budget burn rate is too fast.", severity, conventions.PromSLOServiceLabelName, conventions.PromSLONameLabelName), + "summary": fmt.Sprintf("{{$labels.%s}} {{$labels.%s}} SLO error budget burn rate is over expected.", conventions.PromSLOServiceLabelName, conventions.PromSLONameLabelName), } // Add specific labels. We don't add the labels from the rules because we will // inherit on the alerts, this way we avoid warnings of overrided labels. extraLabels := map[string]string{ - sloSeverityLabelName: severity, + conventions.PromSLOSeverityLabelName: severity, } return &rulefmt.Rule{ diff --git a/internal/prometheus/alert_rules_test.go b/internal/prometheus/alert_rules_test.go index 2320510b..a39648a0 100644 --- a/internal/prometheus/alert_rules_test.go +++ b/internal/prometheus/alert_rules_test.go @@ -8,43 +8,43 @@ import ( "github.com/prometheus/prometheus/model/rulefmt" "github.com/stretchr/testify/assert" - "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/pkg/common/model" ) -func getSLOAlertGroup() alert.MWMBAlertGroup { - return alert.MWMBAlertGroup{ - PageQuick: alert.MWMBAlert{ +func getSLOAlertGroup() model.MWMBAlertGroup { + return model.MWMBAlertGroup{ + PageQuick: model.MWMBAlert{ ID: "10", ShortWindow: 11 * time.Minute, LongWindow: 12 * time.Minute, BurnRateFactor: 13, ErrorBudget: 1, - Severity: alert.PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - PageSlow: alert.MWMBAlert{ + PageSlow: model.MWMBAlert{ ID: "20", ShortWindow: 21 * time.Minute, LongWindow: 22 * time.Minute, BurnRateFactor: 23, ErrorBudget: 1, - Severity: alert.PageAlertSeverity, + Severity: model.PageAlertSeverity, }, - TicketQuick: alert.MWMBAlert{ + TicketQuick: model.MWMBAlert{ ID: "30", ShortWindow: 31 * time.Minute, LongWindow: 32 * time.Minute, BurnRateFactor: 33, ErrorBudget: 1, - Severity: alert.TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, - TicketSlow: alert.MWMBAlert{ + TicketSlow: model.MWMBAlert{ ID: "4", ShortWindow: 41 * time.Minute, LongWindow: 42 * time.Minute, BurnRateFactor: 43, ErrorBudget: 1, - Severity: alert.TicketAlertSeverity, + Severity: model.TicketAlertSeverity, }, } } @@ -52,7 +52,7 @@ func getSLOAlertGroup() alert.MWMBAlertGroup { func TestGenerateSLOAlertRules(t *testing.T) { tests := map[string]struct { slo prometheus.SLO - alertGroup func() alert.MWMBAlertGroup + alertGroup func() model.MWMBAlertGroup expRules []rulefmt.Rule expErr bool }{ diff --git a/internal/prometheus/conventions.go b/internal/prometheus/conventions.go deleted file mode 100644 index a194fcd9..00000000 --- a/internal/prometheus/conventions.go +++ /dev/null @@ -1,17 +0,0 @@ -package prometheus - -const ( - // Metrics. - sliErrorMetricFmt = "slo:sli_error:ratio_rate%s" - - // Labels. - sloNameLabelName = "sloth_slo" - sloIDLabelName = "sloth_id" - sloServiceLabelName = "sloth_service" - sloWindowLabelName = "sloth_window" - sloSeverityLabelName = "sloth_severity" - sloVersionLabelName = "sloth_version" - sloModeLabelName = "sloth_mode" - sloSpecLabelName = "sloth_spec" - sloObjectiveLabelName = "sloth_objective" -) diff --git a/internal/prometheus/helpers.go b/internal/prometheus/helpers.go index 7f40361e..4e8e8c53 100644 --- a/internal/prometheus/helpers.go +++ b/internal/prometheus/helpers.go @@ -5,8 +5,7 @@ import ( "time" prommodel "github.com/prometheus/common/model" - - "github.com/slok/sloth/internal/alert" + "github.com/slok/sloth/pkg/common/model" ) func mergeLabels(ms ...map[string]string) map[string]string { @@ -35,7 +34,7 @@ func timeDurationToPromStr(t time.Duration) string { } // getAlertGroupWindows gets all the time windows from a multiwindow multiburn alert group. -func getAlertGroupWindows(alerts alert.MWMBAlertGroup) []time.Duration { +func getAlertGroupWindows(alerts model.MWMBAlertGroup) []time.Duration { // Use a map to avoid duplicated windows. windows := map[string]time.Duration{ alerts.PageQuick.ShortWindow.String(): alerts.PageQuick.ShortWindow, diff --git a/internal/prometheus/model.go b/internal/prometheus/model.go index fa3a79eb..27bd00df 100644 --- a/internal/prometheus/model.go +++ b/internal/prometheus/model.go @@ -1,288 +1,33 @@ package prometheus import ( - "bytes" "fmt" - "reflect" - "regexp" - "text/template" "time" - "github.com/go-playground/validator/v10" - prommodel "github.com/prometheus/common/model" - "github.com/prometheus/prometheus/model/rulefmt" - promqlparser "github.com/prometheus/prometheus/promql/parser" + "github.com/slok/sloth/pkg/common/conventions" + "github.com/slok/sloth/pkg/common/model" ) -// SLI reprensents an SLI with custom error and total expressions. -type SLI struct { - Raw *SLIRaw - Events *SLIEvents -} - -type SLIRaw struct { - ErrorRatioQuery string `validate:"required,prom_expr,template_vars"` -} - -type SLIEvents struct { - ErrorQuery string `validate:"required,prom_expr,template_vars"` - TotalQuery string `validate:"required,prom_expr,template_vars"` -} - -// AlertMeta is the metadata of an alert settings. -type AlertMeta struct { - Disable bool - Name string `validate:"required_if_enabled"` - Labels map[string]string `validate:"dive,keys,prom_label_key,endkeys,required,prom_label_value"` - Annotations map[string]string `validate:"dive,keys,prom_annot_key,endkeys,required"` -} - -// SLO represents a service level objective configuration. -type SLO struct { - ID string `validate:"required,name"` - Name string `validate:"required,name"` - Description string - Service string `validate:"required,name"` - SLI SLI `validate:"required"` - TimeWindow time.Duration `validate:"required"` - Objective float64 `validate:"gt=0,lte=100"` - Labels map[string]string `validate:"dive,keys,prom_label_key,endkeys,required,prom_label_value"` - PageAlertMeta AlertMeta - TicketAlertMeta AlertMeta -} - -type SLOGroup struct { - SLOs []SLO `validate:"required,dive"` -} - -// Validate validates the SLO. -func (s SLOGroup) Validate() error { - return modelSpecValidate.Struct(s) -} +// TODO(slok): Remove after migration to pkg/common/model package. +type SLO = model.PromSLO +type SLI = model.PromSLI +type SLIEvents = model.PromSLIEvents +type AlertMeta = model.PromAlertMeta +type SLOGroup = model.PromSLOGroup +type SLORules = model.PromSLORules +type SLIRaw = model.PromSLIRaw -// GetSLIErrorMetric returns the SLI error metric. -func (s SLO) GetSLIErrorMetric(window time.Duration) string { - return fmt.Sprintf(sliErrorMetricFmt, timeDurationToPromStr(window)) +// getSLIErrorMetric returns the SLI error metric. +func getSLIErrorMetric(window time.Duration) string { + return fmt.Sprintf(conventions.PromSLIErrorMetricFmt, timeDurationToPromStr(window)) } -// GetSLOIDPromLabels returns the ID labels of an SLO, these can be used to identify +// getSLOIDPromLabels returns the ID labels of an SLO, these can be used to identify // an SLO recorded metrics and alerts. -func (s SLO) GetSLOIDPromLabels() map[string]string { +func getSLOIDPromLabels(s SLO) map[string]string { return map[string]string{ - sloIDLabelName: s.ID, - sloNameLabelName: s.Name, - sloServiceLabelName: s.Service, - } -} - -var modelSpecValidate = func() *validator.Validate { - v := validator.New() - - // More information on prometheus validators logic: https://github.com/prometheus/prometheus/blob/df80dc4d3970121f2f76cba79050983ffb3cdbb0/pkg/rulefmt/rulefmt.go#L188-L208 - mustRegisterValidation(v, "prom_expr", validatePromExpression) - mustRegisterValidation(v, "prom_label_key", validatePromLabelKey) - mustRegisterValidation(v, "prom_label_value", validatePromLabelValue) - mustRegisterValidation(v, "prom_annot_key", validatePromAnnotKey) - mustRegisterValidation(v, "name", validateName) - mustRegisterValidation(v, "required_if_enabled", validateRequiredEnabledAlertName) - mustRegisterValidation(v, "template_vars", validateTemplateVars) - v.RegisterStructValidation(validateOneSLI, SLI{}) - v.RegisterStructValidation(validateSLOGroup, SLOGroup{}) - v.RegisterStructValidation(validateSLIEvents, SLIEvents{}) - return v -}() - -// mustRegisterValidation is a helper so we panic on start if we can't register a validator. -func mustRegisterValidation(v *validator.Validate, tag string, fn validator.Func) { - err := v.RegisterValidation(tag, fn) - if err != nil { - panic(err) - } -} - -// validatePromAnnotKey implements validator.CustomTypeFunc by validating -// a prometheus annotation key. -func validatePromAnnotKey(fl validator.FieldLevel) bool { - k, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - return prommodel.LabelName(k).IsValid() -} - -// validatePromLabel implements validator.CustomTypeFunc by validating -// a prometheus label key. -func validatePromLabelKey(fl validator.FieldLevel) bool { - k, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - return prommodel.LabelName(k).IsValid() && k != prommodel.MetricNameLabel -} - -// validatePromLabelValue implements validator.CustomTypeFunc by validating -// a prometheus label value. -func validatePromLabelValue(fl validator.FieldLevel) bool { - v, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - return prommodel.LabelValue(v).IsValid() -} - -var promExprTplAllowedFakeData = map[string]string{ - "window": "1m", -} - -// validatePromExpression implements validator.CustomTypeFunc by validating -// a prometheus expression. -func validatePromExpression(fl validator.FieldLevel) bool { - expr, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - // The expressions set by users can have some allowed templated data - // we are rendering the expression with fake data so prometheus can - // have a final expr and check if is correct. - tpl, err := template.New("expr").Parse(expr) - if err != nil { - return false - } - - var tplB bytes.Buffer - err = tpl.Execute(&tplB, promExprTplAllowedFakeData) - if err != nil { - return false - } - - _, err = promqlparser.ParseExpr(tplB.String()) - return err == nil -} - -// Names must: -// - Start and end with an alphanumeric. -// - Contain alphanumeric, `.`, '_', and '-'. -var ( - nameRegexp = regexp.MustCompile("^[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]$") -) - -// validateName implements validator.CustomTypeFunc by validating -// a regular name. -func validateName(fl validator.FieldLevel) bool { - s, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - return nameRegexp.MatchString(s) -} - -func validateRequiredEnabledAlertName(fl validator.FieldLevel) bool { - alertMeta, ok := fl.Parent().Interface().(AlertMeta) - if !ok { - return false - } - - if alertMeta.Disable { - return true - } - - return alertMeta.Name != "" -} - -var tplWindowRegex = regexp.MustCompile(fmt.Sprintf(`{{ *\.%s *}}`, tplKeyWindow)) - -// validateTemplateVars implements validator.CustomTypeFunc by validating -// an SLI template has all the required fields. -func validateTemplateVars(fl validator.FieldLevel) bool { - v, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - return tplWindowRegex.MatchString(v) -} - -// validateSLIEvents validates that both SLI event queries are different. -func validateSLIEvents(sl validator.StructLevel) { - s, ok := sl.Current().Interface().(SLIEvents) - if !ok { - sl.ReportError(s, "", "SLIEvents", "not_sli_events", "") - return + conventions.PromSLOIDLabelName: s.ID, + conventions.PromSLONameLabelName: s.Name, + conventions.PromSLOServiceLabelName: s.Service, } - - // If empty we don't need to check. - if s.ErrorQuery == "" || s.TotalQuery == "" { - return - } - - // If different, they are valid. - if s.ErrorQuery == s.TotalQuery { - sl.ReportError(s, "", "", "sli_events_queries_different", "") - return - } -} - -// validateOneSLI validates only one SLI type is set and configured. -func validateOneSLI(sl validator.StructLevel) { - sli, ok := sl.Current().Interface().(SLI) - if !ok { - sl.ReportError(sli, "", "SLI", "not_sli", "") - return - } - - // Check only one SLI type is set. - sliSet := false - sliType := reflect.ValueOf(sli) - strNumFields := sliType.NumField() - for i := 0; i < strNumFields; i++ { - f := sliType.Field(i) - if f.IsNil() { - continue - } - // We already have one SLI type set. - if sliSet { - sl.ReportError(sli, "", "", "one_sli_type", "") - } - sliSet = true - } - - // No SLI types set. - if !sliSet { - sl.ReportError(sli, "", "", "sli_type_required", "") - } -} - -// validateSLOGroup validates SLO IDs are not repeated. -func validateSLOGroup(sl validator.StructLevel) { - sloGroup, ok := sl.Current().Interface().(SLOGroup) - if !ok { - sl.ReportError(sloGroup, "", "SLOGroup", "not_slo_group", "") - return - } - - if len(sloGroup.SLOs) == 0 { - sl.ReportError(sloGroup, "", "", "slos_required", "") - } - - // Check SLO IDs not repeated. - sloIDs := map[string]struct{}{} - for _, slo := range sloGroup.SLOs { - _, ok := sloIDs[slo.ID] - if ok { - sl.ReportError(slo.ID, slo.ID, "", "slo_repeated", "") - } - sloIDs[slo.ID] = struct{}{} - } -} - -// SLORules are the prometheus rules required by an SLO. -type SLORules struct { - SLIErrorRecRules []rulefmt.Rule - MetadataRecRules []rulefmt.Rule - AlertRules []rulefmt.Rule } diff --git a/internal/prometheus/recording_rules.go b/internal/prometheus/recording_rules.go index b9f4a6c4..81582257 100644 --- a/internal/prometheus/recording_rules.go +++ b/internal/prometheus/recording_rules.go @@ -10,12 +10,12 @@ import ( "github.com/prometheus/prometheus/model/rulefmt" - "github.com/slok/sloth/internal/alert" - "github.com/slok/sloth/internal/info" + "github.com/slok/sloth/pkg/common/conventions" + "github.com/slok/sloth/pkg/common/model" ) // sliRulesgenFunc knows how to generate an SLI recording rule for a specific time window. -type sliRulesgenFunc func(slo SLO, window time.Duration, alerts alert.MWMBAlertGroup) (*rulefmt.Rule, error) +type sliRulesgenFunc func(slo SLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) type sliRecordingRulesGenerator struct { genFunc sliRulesgenFunc @@ -31,7 +31,7 @@ var OptimizedSLIRecordingRulesGenerator = sliRecordingRulesGenerator{genFunc: op // Normally these rules are used by the SLO alerts. var SLIRecordingRulesGenerator = sliRecordingRulesGenerator{genFunc: factorySLIRecordGenerator} -func optimizedFactorySLIRecordGenerator(slo SLO, window time.Duration, alerts alert.MWMBAlertGroup) (*rulefmt.Rule, error) { +func optimizedFactorySLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { // Optimize the rules that are for the total period time window. if window == slo.TimeWindow { return optimizedSLIRecordGenerator(slo, window, alerts.PageQuick.ShortWindow) @@ -40,7 +40,7 @@ func optimizedFactorySLIRecordGenerator(slo SLO, window time.Duration, alerts al return factorySLIRecordGenerator(slo, window, alerts) } -func (s sliRecordingRulesGenerator) GenerateSLIRecordingRules(ctx context.Context, slo SLO, alerts alert.MWMBAlertGroup) ([]rulefmt.Rule, error) { +func (s sliRecordingRulesGenerator) GenerateSLIRecordingRules(ctx context.Context, slo SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { // Get the windows we need the recording rules. windows := getAlertGroupWindows(alerts) windows = append(windows, slo.TimeWindow) // Add the total time window as a handy helper. @@ -62,7 +62,7 @@ const ( tplKeyWindow = "window" ) -func factorySLIRecordGenerator(slo SLO, window time.Duration, alerts alert.MWMBAlertGroup) (*rulefmt.Rule, error) { +func factorySLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { switch { // Event based SLI. case slo.SLI.Events != nil: @@ -75,7 +75,7 @@ func factorySLIRecordGenerator(slo SLO, window time.Duration, alerts alert.MWMBA return nil, fmt.Errorf("invalid SLI type") } -func rawSLIRecordGenerator(slo SLO, window time.Duration, alerts alert.MWMBAlertGroup) (*rulefmt.Rule, error) { +func rawSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { // Render with our templated data. sliExprTpl := fmt.Sprintf(`(%s)`, slo.SLI.Raw.ErrorRatioQuery) tpl, err := template.New("sliExpr").Option("missingkey=error").Parse(sliExprTpl) @@ -93,19 +93,19 @@ func rawSLIRecordGenerator(slo SLO, window time.Duration, alerts alert.MWMBAlert } return &rulefmt.Rule{ - Record: slo.GetSLIErrorMetric(window), + Record: getSLIErrorMetric(window), Expr: b.String(), Labels: mergeLabels( - slo.GetSLOIDPromLabels(), + getSLOIDPromLabels(slo), map[string]string{ - sloWindowLabelName: strWindow, + conventions.PromSLOWindowLabelName: strWindow, }, slo.Labels, ), }, nil } -func eventsSLIRecordGenerator(slo SLO, window time.Duration, alerts alert.MWMBAlertGroup) (*rulefmt.Rule, error) { +func eventsSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { const sliExprTplFmt = `(%s) / (%s) @@ -129,12 +129,12 @@ func eventsSLIRecordGenerator(slo SLO, window time.Duration, alerts alert.MWMBAl } return &rulefmt.Rule{ - Record: slo.GetSLIErrorMetric(window), + Record: getSLIErrorMetric(window), Expr: b.String(), Labels: mergeLabels( - slo.GetSLOIDPromLabels(), + getSLOIDPromLabels(slo), map[string]string{ - sloWindowLabelName: strWindow, + conventions.PromSLOWindowLabelName: strWindow, }, slo.Labels, ), @@ -162,8 +162,8 @@ count_over_time({{.metric}}{{.filter}}[{{.window}}]) return nil, fmt.Errorf("can't optimize using the same shortwindow as the window to optimize") } - shortWindowSLIRec := slo.GetSLIErrorMetric(shortWindow) - filter := labelsToPromFilter(slo.GetSLOIDPromLabels()) + shortWindowSLIRec := getSLIErrorMetric(shortWindow) + filter := labelsToPromFilter(getSLOIDPromLabels(slo)) // Render with our templated data. tpl, err := template.New("sliExpr").Option("missingkey=error").Parse(sliExprTplFmt) @@ -177,19 +177,19 @@ count_over_time({{.metric}}{{.filter}}[{{.window}}]) "metric": shortWindowSLIRec, "filter": filter, "window": strWindow, - "windowKey": sloWindowLabelName, + "windowKey": conventions.PromSLOWindowLabelName, }) if err != nil { return nil, fmt.Errorf("could not render SLI expression template: %w", err) } return &rulefmt.Rule{ - Record: slo.GetSLIErrorMetric(window), + Record: getSLIErrorMetric(window), Expr: b.String(), Labels: mergeLabels( - slo.GetSLOIDPromLabels(), + getSLOIDPromLabels(slo), map[string]string{ - sloWindowLabelName: strWindow, + conventions.PromSLOWindowLabelName: strWindow, }, slo.Labels, ), @@ -202,8 +202,8 @@ type metadataRecordingRulesGenerator bool // from an SLO. const MetadataRecordingRulesGenerator = metadataRecordingRulesGenerator(false) -func (m metadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx context.Context, info info.Info, slo SLO, alerts alert.MWMBAlertGroup) ([]rulefmt.Rule, error) { - labels := mergeLabels(slo.GetSLOIDPromLabels(), slo.Labels) +func (m metadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx context.Context, info model.Info, slo SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { + labels := mergeLabels(getSLOIDPromLabels(slo), slo.Labels) // Metatada Recordings. const ( @@ -218,15 +218,15 @@ func (m metadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx cont sloObjectiveRatio := slo.Objective / 100 - sloFilter := labelsToPromFilter(slo.GetSLOIDPromLabels()) + sloFilter := labelsToPromFilter(getSLOIDPromLabels(slo)) var currentBurnRateExpr bytes.Buffer err := burnRateRecordingExprTpl.Execute(¤tBurnRateExpr, map[string]string{ - "SLIErrorMetric": slo.GetSLIErrorMetric(alerts.PageQuick.ShortWindow), + "SLIErrorMetric": getSLIErrorMetric(alerts.PageQuick.ShortWindow), "MetricFilter": sloFilter, - "SLOIDName": sloIDLabelName, - "SLOLabelName": sloNameLabelName, - "SLOServiceName": sloServiceLabelName, + "SLOIDName": conventions.PromSLOIDLabelName, + "SLOLabelName": conventions.PromSLONameLabelName, + "SLOServiceName": conventions.PromSLOServiceLabelName, "ErrorBudgetRatioMetric": metricSLOErrorBudgetRatio, }) if err != nil { @@ -235,11 +235,11 @@ func (m metadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx cont var periodBurnRateExpr bytes.Buffer err = burnRateRecordingExprTpl.Execute(&periodBurnRateExpr, map[string]string{ - "SLIErrorMetric": slo.GetSLIErrorMetric(slo.TimeWindow), + "SLIErrorMetric": getSLIErrorMetric(slo.TimeWindow), "MetricFilter": sloFilter, - "SLOIDName": sloIDLabelName, - "SLOLabelName": sloNameLabelName, - "SLOServiceName": sloServiceLabelName, + "SLOIDName": conventions.PromSLOIDLabelName, + "SLOLabelName": conventions.PromSLONameLabelName, + "SLOServiceName": conventions.PromSLOServiceLabelName, "ErrorBudgetRatioMetric": metricSLOErrorBudgetRatio, }) if err != nil { @@ -294,10 +294,10 @@ func (m metadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx cont Record: metricSLOInfo, Expr: `vector(1)`, Labels: mergeLabels(labels, map[string]string{ - sloVersionLabelName: info.Version, - sloModeLabelName: string(info.Mode), - sloSpecLabelName: info.Spec, - sloObjectiveLabelName: strconv.FormatFloat(slo.Objective, 'f', -1, 64), + conventions.PromSLOVersionLabelName: info.Version, + conventions.PromSLOModeLabelName: string(info.Mode), + conventions.PromSLOSpecLabelName: info.Spec, + conventions.PromSLOObjectiveLabelName: strconv.FormatFloat(slo.Objective, 'f', -1, 64), }), }, } diff --git a/internal/prometheus/recording_rules_test.go b/internal/prometheus/recording_rules_test.go index 6e518d78..ff1624b6 100644 --- a/internal/prometheus/recording_rules_test.go +++ b/internal/prometheus/recording_rules_test.go @@ -8,26 +8,25 @@ import ( "github.com/prometheus/prometheus/model/rulefmt" "github.com/stretchr/testify/assert" - "github.com/slok/sloth/internal/alert" - "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/pkg/common/model" ) -func getAlertGroup() alert.MWMBAlertGroup { - return alert.MWMBAlertGroup{ - PageQuick: alert.MWMBAlert{ +func getAlertGroup() model.MWMBAlertGroup { + return model.MWMBAlertGroup{ + PageQuick: model.MWMBAlert{ ShortWindow: 5 * time.Minute, LongWindow: 1 * time.Hour, }, - PageSlow: alert.MWMBAlert{ + PageSlow: model.MWMBAlert{ ShortWindow: 30 * time.Minute, LongWindow: 6 * time.Hour, }, - TicketQuick: alert.MWMBAlert{ + TicketQuick: model.MWMBAlert{ ShortWindow: 2 * time.Hour, LongWindow: 1 * 24 * time.Hour, }, - TicketSlow: alert.MWMBAlert{ + TicketSlow: model.MWMBAlert{ ShortWindow: 6 * time.Hour, LongWindow: 3 * 24 * time.Hour, }, @@ -36,13 +35,13 @@ func getAlertGroup() alert.MWMBAlertGroup { func TestGenerateSLIRecordingRules(t *testing.T) { type generator interface { - GenerateSLIRecordingRules(ctx context.Context, slo prometheus.SLO, alerts alert.MWMBAlertGroup) ([]rulefmt.Rule, error) + GenerateSLIRecordingRules(ctx context.Context, slo prometheus.SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) } tests := map[string]struct { generator func() generator slo prometheus.SLO - alertGroup alert.MWMBAlertGroup + alertGroup model.MWMBAlertGroup expRules []rulefmt.Rule expErr bool }{ @@ -434,11 +433,11 @@ func TestGenerateSLIRecordingRules(t *testing.T) { "kind": "test", }, }, - alertGroup: alert.MWMBAlertGroup{ - PageQuick: alert.MWMBAlert{ShortWindow: 3 * time.Hour, LongWindow: 2 * time.Hour}, - PageSlow: alert.MWMBAlert{ShortWindow: 3 * time.Hour, LongWindow: 1 * time.Hour}, - TicketQuick: alert.MWMBAlert{ShortWindow: 1 * time.Hour, LongWindow: 2 * time.Hour}, - TicketSlow: alert.MWMBAlert{ShortWindow: 2 * time.Hour, LongWindow: 1 * time.Hour}, + alertGroup: model.MWMBAlertGroup{ + PageQuick: model.MWMBAlert{ShortWindow: 3 * time.Hour, LongWindow: 2 * time.Hour}, + PageSlow: model.MWMBAlert{ShortWindow: 3 * time.Hour, LongWindow: 1 * time.Hour}, + TicketQuick: model.MWMBAlert{ShortWindow: 1 * time.Hour, LongWindow: 2 * time.Hour}, + TicketSlow: model.MWMBAlert{ShortWindow: 2 * time.Hour, LongWindow: 1 * time.Hour}, }, expRules: []rulefmt.Rule{ { @@ -506,16 +505,16 @@ func TestGenerateSLIRecordingRules(t *testing.T) { func TestGenerateMetaRecordingRules(t *testing.T) { tests := map[string]struct { - info info.Info + info model.Info slo prometheus.SLO - alertGroup alert.MWMBAlertGroup + alertGroup model.MWMBAlertGroup expRules []rulefmt.Rule expErr bool }{ "Having and SLO an its mwmb alerts should create the metadata recording rules.": { - info: info.Info{ + info: model.Info{ Version: "test-ver", - Mode: info.ModeTest, + Mode: model.ModeTest, Spec: "test/v1", }, slo: prometheus.SLO{ diff --git a/pkg/common/conventions/conventions.go b/pkg/common/conventions/conventions.go new file mode 100644 index 00000000..a3c8becc --- /dev/null +++ b/pkg/common/conventions/conventions.go @@ -0,0 +1,20 @@ +package conventions + +// Prometheus metrics conventions. +const ( + // Metrics. + PromSLIErrorMetricFmt = "slo:sli_error:ratio_rate%s" + + // Labels. + PromSLONameLabelName = "sloth_slo" + PromSLOIDLabelName = "sloth_id" + PromSLOServiceLabelName = "sloth_service" + PromSLOWindowLabelName = "sloth_window" + PromSLOSeverityLabelName = "sloth_severity" + PromSLOVersionLabelName = "sloth_version" + PromSLOModeLabelName = "sloth_mode" + PromSLOSpecLabelName = "sloth_spec" + PromSLOObjectiveLabelName = "sloth_objective" + + PromQueryTPLKeyWindow = "window" +) diff --git a/pkg/common/model/alert.go b/pkg/common/model/alert.go new file mode 100644 index 00000000..900a22b4 --- /dev/null +++ b/pkg/common/model/alert.go @@ -0,0 +1,46 @@ +package model + +import "time" + +// AlertSeverity is the type of alert. +type AlertSeverity int + +const ( + UnknownAlertSeverity AlertSeverity = iota + PageAlertSeverity + TicketAlertSeverity +) + +func (s AlertSeverity) String() string { + switch s { + case PageAlertSeverity: + return "page" + case TicketAlertSeverity: + return "ticket" + default: + return "unknown" + } +} + +// MWMBAlert represents a multiwindow, multi-burn rate alert. +type MWMBAlert struct { + ID string + ShortWindow time.Duration + LongWindow time.Duration + BurnRateFactor float64 + ErrorBudget float64 + Severity AlertSeverity +} + +// MWMBAlertGroup what represents all the alerts of an SLO. +// ITs divided into two groups that are made of 2 alerts: +// - Page & quick: Critical alerts that trigger in high rate burn in short term. +// - Page & slow: Critical alerts that trigger in high-normal rate burn in medium term. +// - Ticket & slow: Warning alerts that trigger in normal rate burn in medium term. +// - Ticket & slow: Warning alerts that trigger in slow rate burn in long term. +type MWMBAlertGroup struct { + PageQuick MWMBAlert + PageSlow MWMBAlert + TicketQuick MWMBAlert + TicketSlow MWMBAlert +} diff --git a/pkg/common/model/info.go b/pkg/common/model/info.go new file mode 100644 index 00000000..28adaf27 --- /dev/null +++ b/pkg/common/model/info.go @@ -0,0 +1,18 @@ +package model + +type Mode string + +const ( + ModeTest = "test" + ModeCLIGenPrometheus = "cli-gen-prom" + ModeCLIGenKubernetes = "cli-gen-k8s" + ModeCLIGenOpenSLO = "cli-gen-openslo" + ModeControllerGenKubernetes = "ctrl-gen-k8s" +) + +// Info is the information of the app and request based for SLO generators. +type Info struct { + Version string + Mode Mode + Spec string +} diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go new file mode 100644 index 00000000..b9236df6 --- /dev/null +++ b/pkg/common/model/slo_prometheus.go @@ -0,0 +1,274 @@ +package model + +import ( + "bytes" + "fmt" + "reflect" + "regexp" + "text/template" + "time" + + "github.com/go-playground/validator/v10" + prommodel "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/rulefmt" + promqlparser "github.com/prometheus/prometheus/promql/parser" + "github.com/slok/sloth/pkg/common/conventions" +) + +// SLI represents an SLI with custom error and total expressions. +type PromSLI struct { + Raw *PromSLIRaw + Events *PromSLIEvents +} + +type PromSLIRaw struct { + ErrorRatioQuery string `validate:"required,prom_expr,template_vars"` +} + +type PromSLIEvents struct { + ErrorQuery string `validate:"required,prom_expr,template_vars"` + TotalQuery string `validate:"required,prom_expr,template_vars"` +} + +// AlertMeta is the metadata of an alert settings. +type PromAlertMeta struct { + Disable bool + Name string `validate:"required_if_enabled"` + Labels map[string]string `validate:"dive,keys,prom_label_key,endkeys,required,prom_label_value"` + Annotations map[string]string `validate:"dive,keys,prom_annot_key,endkeys,required"` +} + +// PromSLO represents a service level objective configuration. +type PromSLO struct { + ID string `validate:"required,name"` + Name string `validate:"required,name"` + Description string + Service string `validate:"required,name"` + SLI PromSLI `validate:"required"` + TimeWindow time.Duration `validate:"required"` + Objective float64 `validate:"gt=0,lte=100"` + Labels map[string]string `validate:"dive,keys,prom_label_key,endkeys,required,prom_label_value"` + PageAlertMeta PromAlertMeta + TicketAlertMeta PromAlertMeta +} + +type PromSLOGroup struct { + SLOs []PromSLO `validate:"required,dive"` +} + +// Validate validates the SLO. +func (s PromSLOGroup) Validate() error { + return modelSpecValidate.Struct(s) +} + +var modelSpecValidate = func() *validator.Validate { + v := validator.New() + + // More information on prometheus validators logic: https://github.com/prometheus/prometheus/blob/df80dc4d3970121f2f76cba79050983ffb3cdbb0/pkg/rulefmt/rulefmt.go#L188-L208 + mustRegisterValidation(v, "prom_expr", validatePromExpression) + mustRegisterValidation(v, "prom_label_key", validatePromLabelKey) + mustRegisterValidation(v, "prom_label_value", validatePromLabelValue) + mustRegisterValidation(v, "prom_annot_key", validatePromAnnotKey) + mustRegisterValidation(v, "name", validateName) + mustRegisterValidation(v, "required_if_enabled", validateRequiredEnabledAlertName) + mustRegisterValidation(v, "template_vars", validateTemplateVars) + v.RegisterStructValidation(validateOneSLI, PromSLI{}) + v.RegisterStructValidation(validateSLOGroup, PromSLOGroup{}) + v.RegisterStructValidation(validateSLIEvents, PromSLIEvents{}) + return v +}() + +// mustRegisterValidation is a helper so we panic on start if we can't register a validator. +func mustRegisterValidation(v *validator.Validate, tag string, fn validator.Func) { + err := v.RegisterValidation(tag, fn) + if err != nil { + panic(err) + } +} + +// validatePromAnnotKey implements validator.CustomTypeFunc by validating +// a prometheus annotation key. +func validatePromAnnotKey(fl validator.FieldLevel) bool { + k, ok := fl.Field().Interface().(string) + if !ok { + return false + } + + return prommodel.LabelName(k).IsValid() +} + +// validatePromLabel implements validator.CustomTypeFunc by validating +// a prometheus label key. +func validatePromLabelKey(fl validator.FieldLevel) bool { + k, ok := fl.Field().Interface().(string) + if !ok { + return false + } + + return prommodel.LabelName(k).IsValid() && k != prommodel.MetricNameLabel +} + +// validatePromLabelValue implements validator.CustomTypeFunc by validating +// a prometheus label value. +func validatePromLabelValue(fl validator.FieldLevel) bool { + v, ok := fl.Field().Interface().(string) + if !ok { + return false + } + + return prommodel.LabelValue(v).IsValid() +} + +var promExprTplAllowedFakeData = map[string]string{ + "window": "1m", +} + +// validatePromExpression implements validator.CustomTypeFunc by validating +// a prometheus expression. +func validatePromExpression(fl validator.FieldLevel) bool { + expr, ok := fl.Field().Interface().(string) + if !ok { + return false + } + + // The expressions set by users can have some allowed templated data + // we are rendering the expression with fake data so prometheus can + // have a final expr and check if is correct. + tpl, err := template.New("expr").Parse(expr) + if err != nil { + return false + } + + var tplB bytes.Buffer + err = tpl.Execute(&tplB, promExprTplAllowedFakeData) + if err != nil { + return false + } + + _, err = promqlparser.ParseExpr(tplB.String()) + return err == nil +} + +// Names must: +// - Start and end with an alphanumeric. +// - Contain alphanumeric, `.`, '_', and '-'. +var ( + nameRegexp = regexp.MustCompile("^[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]$") +) + +// validateName implements validator.CustomTypeFunc by validating +// a regular name. +func validateName(fl validator.FieldLevel) bool { + s, ok := fl.Field().Interface().(string) + if !ok { + return false + } + + return nameRegexp.MatchString(s) +} + +func validateRequiredEnabledAlertName(fl validator.FieldLevel) bool { + alertMeta, ok := fl.Parent().Interface().(PromAlertMeta) + if !ok { + return false + } + + if alertMeta.Disable { + return true + } + + return alertMeta.Name != "" +} + +var tplWindowRegex = regexp.MustCompile(fmt.Sprintf(`{{ *\.%s *}}`, conventions.PromQueryTPLKeyWindow)) + +// validateTemplateVars implements validator.CustomTypeFunc by validating +// an SLI template has all the required fields. +func validateTemplateVars(fl validator.FieldLevel) bool { + v, ok := fl.Field().Interface().(string) + if !ok { + return false + } + + return tplWindowRegex.MatchString(v) +} + +// validateSLIEvents validates that both SLI event queries are different. +func validateSLIEvents(sl validator.StructLevel) { + s, ok := sl.Current().Interface().(PromSLIEvents) + if !ok { + sl.ReportError(s, "", "SLIEvents", "not_sli_events", "") + return + } + + // If empty we don't need to check. + if s.ErrorQuery == "" || s.TotalQuery == "" { + return + } + + // If different, they are valid. + if s.ErrorQuery == s.TotalQuery { + sl.ReportError(s, "", "", "sli_events_queries_different", "") + return + } +} + +// validateOneSLI validates only one SLI type is set and configured. +func validateOneSLI(sl validator.StructLevel) { + sli, ok := sl.Current().Interface().(PromSLI) + if !ok { + sl.ReportError(sli, "", "SLI", "not_sli", "") + return + } + + // Check only one SLI type is set. + sliSet := false + sliType := reflect.ValueOf(sli) + strNumFields := sliType.NumField() + for i := 0; i < strNumFields; i++ { + f := sliType.Field(i) + if f.IsNil() { + continue + } + // We already have one SLI type set. + if sliSet { + sl.ReportError(sli, "", "", "one_sli_type", "") + } + sliSet = true + } + + // No SLI types set. + if !sliSet { + sl.ReportError(sli, "", "", "sli_type_required", "") + } +} + +// validateSLOGroup validates SLO IDs are not repeated. +func validateSLOGroup(sl validator.StructLevel) { + sloGroup, ok := sl.Current().Interface().(PromSLOGroup) + if !ok { + sl.ReportError(sloGroup, "", "SLOGroup", "not_slo_group", "") + return + } + + if len(sloGroup.SLOs) == 0 { + sl.ReportError(sloGroup, "", "", "slos_required", "") + } + + // Check SLO IDs not repeated. + sloIDs := map[string]struct{}{} + for _, slo := range sloGroup.SLOs { + _, ok := sloIDs[slo.ID] + if ok { + sl.ReportError(slo.ID, slo.ID, "", "slo_repeated", "") + } + sloIDs[slo.ID] = struct{}{} + } +} + +// PromSLORules are the prometheus rules required by an SLO. +type PromSLORules struct { + SLIErrorRecRules []rulefmt.Rule + MetadataRecRules []rulefmt.Rule + AlertRules []rulefmt.Rule +} diff --git a/internal/prometheus/model_test.go b/pkg/common/model/slo_prometheus_test.go similarity index 53% rename from internal/prometheus/model_test.go rename to pkg/common/model/slo_prometheus_test.go index 8d53fd22..2529e364 100644 --- a/internal/prometheus/model_test.go +++ b/pkg/common/model/slo_prometheus_test.go @@ -1,4 +1,4 @@ -package prometheus_test +package model_test import ( "testing" @@ -6,19 +6,19 @@ import ( "github.com/stretchr/testify/assert" - "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/pkg/common/model" ) -func getGoodSLOGroup() prometheus.SLOGroup { - return prometheus.SLOGroup{ - SLOs: []prometheus.SLO{ +func getGoodSLOGroup() model.PromSLOGroup { + return model.PromSLOGroup{ + SLOs: []model.PromSLO{ { ID: "slo1-id", Name: "test.slo-0_1", Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ ErrorQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))`, TotalQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp"}[{{ .window }}]))`, }, @@ -28,7 +28,7 @@ func getGoodSLOGroup() prometheus.SLOGroup { "owner": "myteam", "category": "test", }, - PageAlertMeta: prometheus.AlertMeta{ + PageAlertMeta: model.PromAlertMeta{ Disable: false, Name: "testAlert", Labels: map[string]string{ @@ -41,7 +41,7 @@ func getGoodSLOGroup() prometheus.SLOGroup { "runbook": "http://whatever.com", }, }, - TicketAlertMeta: prometheus.AlertMeta{ + TicketAlertMeta: model.PromAlertMeta{ Disable: false, Name: "testAlert", Labels: map[string]string{ @@ -61,272 +61,272 @@ func getGoodSLOGroup() prometheus.SLOGroup { func TestModelValidationSpec(t *testing.T) { tests := map[string]struct { - slo func() prometheus.SLOGroup + slo func() model.PromSLOGroup expErrMessage string }{ "Correct SLO should not fail.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { return getGoodSLOGroup() }, }, "SLOs must exist.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() - s.SLOs = []prometheus.SLO{} + s.SLOs = []model.PromSLO{} return s }, - expErrMessage: "Key: 'SLOGroup.' Error:Field validation for '' failed on the 'slos_required' tag", + expErrMessage: "Key: 'PromSLOGroup.' Error:Field validation for '' failed on the 'slos_required' tag", }, "SLOs can't be repeated.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs = append(s.SLOs, s.SLOs[0]) return s }, - expErrMessage: "Key: 'SLOGroup.slo1-id' Error:Field validation for 'slo1-id' failed on the 'slo_repeated' tag", + expErrMessage: "Key: 'PromSLOGroup.slo1-id' Error:Field validation for 'slo1-id' failed on the 'slo_repeated' tag", }, "SLO ID is required.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].ID = "" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'required' tag", }, "SLO ID must be alphanumeric, `.`, '_', and '-'.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].ID = "this-is-{a-test" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'name' tag", }, "SLO ID must start with aphanumeric.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].ID = "_" + s.SLOs[0].ID return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'name' tag", }, "SLO ID must end with aphanumeric.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].ID = s.SLOs[0].ID + "_" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'name' tag", }, "SLO Name is required.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Name = "" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'required' tag", }, "SLO Name must be alphanumeric, `.`, '_', and '-'.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Name = "this-is-{a-test" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'name' tag", }, "SLO Name must start with aphanumeric.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Name = "_" + s.SLOs[0].Name return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'name' tag", }, "SLO Name must end with aphanumeric.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Name = s.SLOs[0].Name + "_" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'name' tag", }, "SLO Service is required.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Service = "" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'required' tag", }, "SLO Service must be alphanumeric, `.`, '_', and '-'.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Service = "this-is-{a-test" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'name' tag", }, "SLO Service must start with aphanumeric.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Service = "_" + s.SLOs[0].Service return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'name' tag", }, "SLO Service must end with aphanumeric.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Service = s.SLOs[0].Service + "_" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'name' tag", }, "SLO without SLI type should fail.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() - s.SLOs[0].SLI = prometheus.SLI{} + s.SLOs[0].SLI = model.PromSLI{} return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].SLI.' Error:Field validation for '' failed on the 'sli_type_required' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.' Error:Field validation for '' failed on the 'sli_type_required' tag", }, "SLO with more than one SLI type should fail.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() - s.SLOs[0].SLI.Raw = &prometheus.SLIRaw{ + s.SLOs[0].SLI.Raw = &model.PromSLIRaw{ ErrorRatioQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))`, } return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].SLI.' Error:Field validation for '' failed on the 'one_sli_type' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.' Error:Field validation for '' failed on the 'one_sli_type' tag", }, "SLO SLI event queries must be different.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].SLI.Events.TotalQuery = `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))` s.SLOs[0].SLI.Events.ErrorQuery = `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))` return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].SLI.Events.' Error:Field validation for '' failed on the 'sli_events_queries_different' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.Events.' Error:Field validation for '' failed on the 'sli_events_queries_different' tag", }, "SLO SLI error query should be valid Prometheus expr.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].SLI.Events.ErrorQuery = "sum(rate(grpc_server_handled_requests_count{[1m]))" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].SLI.Events.ErrorQuery' Error:Field validation for 'ErrorQuery' failed on the 'prom_expr' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.Events.ErrorQuery' Error:Field validation for 'ErrorQuery' failed on the 'prom_expr' tag", }, "SLO SLI error query should have required template vars.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].SLI.Events.ErrorQuery = "sum(rate(grpc_server_handled_requests_count[1m]))" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].SLI.Events.ErrorQuery' Error:Field validation for 'ErrorQuery' failed on the 'template_vars' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.Events.ErrorQuery' Error:Field validation for 'ErrorQuery' failed on the 'template_vars' tag", }, "SLO SLI total query should be valid Prometheus expr.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].SLI.Events.TotalQuery = "sum(rate(grpc_server_handled_requests_count{[1m]))" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].SLI.Events.TotalQuery' Error:Field validation for 'TotalQuery' failed on the 'prom_expr' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.Events.TotalQuery' Error:Field validation for 'TotalQuery' failed on the 'prom_expr' tag", }, "SLO SLI total query should have required template vars.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].SLI.Events.TotalQuery = "sum(rate(grpc_server_handled_requests_count[1m]))" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].SLI.Events.TotalQuery' Error:Field validation for 'TotalQuery' failed on the 'template_vars' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.Events.TotalQuery' Error:Field validation for 'TotalQuery' failed on the 'template_vars' tag", }, "SLO Objective shouldn't be less than 0.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Objective = -1 return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Objective' Error:Field validation for 'Objective' failed on the 'gt' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Objective' Error:Field validation for 'Objective' failed on the 'gt' tag", }, "SLO Objective shouldn't be 0.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Objective = 0 return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Objective' Error:Field validation for 'Objective' failed on the 'gt' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Objective' Error:Field validation for 'Objective' failed on the 'gt' tag", }, "SLO Objective shouldn't be greater than 100.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Objective = 100.0001 return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Objective' Error:Field validation for 'Objective' failed on the 'lte' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Objective' Error:Field validation for 'Objective' failed on the 'lte' tag", }, "SLO Labels should be valid prometheus keys.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", }, "SLO Labels should have prometheus values.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Labels["something"] = "" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", }, "SLO Labels should be valid prometheus values.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].Labels["something"] = "\xc3\x28" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", }, "SLO page alert name is required.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].PageAlertMeta.Name = "" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].PageAlertMeta.Name' Error:Field validation for 'Name' failed on the 'required_if_enabled' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Name' Error:Field validation for 'Name' failed on the 'required_if_enabled' tag", }, "SLO page alert fields are not required if disabled .": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].PageAlertMeta.Name = "" s.SLOs[0].PageAlertMeta.Disable = true @@ -337,16 +337,16 @@ func TestModelValidationSpec(t *testing.T) { }, "SLO warning alert name is required.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].TicketAlertMeta.Name = "" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].TicketAlertMeta.Name' Error:Field validation for 'Name' failed on the 'required_if_enabled' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Name' Error:Field validation for 'Name' failed on the 'required_if_enabled' tag", }, "SLO warning alert fields are not required if disabled .": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].TicketAlertMeta.Name = "" s.SLOs[0].TicketAlertMeta.Disable = true @@ -357,93 +357,93 @@ func TestModelValidationSpec(t *testing.T) { }, "SLO page alert labels should be valid prometheus keys.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].PageAlertMeta.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].PageAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", }, "SLO page alert labels should have prometheus values.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].PageAlertMeta.Labels["something"] = "" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].PageAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", }, "SLO page alert labels should be valid prometheus values.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].PageAlertMeta.Labels["something"] = "\xc3\x28" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].PageAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", }, "SLO page alert annotations should be valid prometheus keys.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].PageAlertMeta.Annotations["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].PageAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", }, "SLO page alert annotations should have prometheus values.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].PageAlertMeta.Annotations["something"] = "" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].PageAlertMeta.Annotations[something]' Error:Field validation for 'Annotations[something]' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Annotations[something]' Error:Field validation for 'Annotations[something]' failed on the 'required' tag", }, "SLO warning alert labels should be valid prometheus keys.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].TicketAlertMeta.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].TicketAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", }, "SLO warning alert labels should have prometheus values.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].TicketAlertMeta.Labels["something"] = "" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].TicketAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", }, "SLO warning alert labels should be valid prometheus values.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].TicketAlertMeta.Labels["something"] = "\xc3\x28" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].TicketAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", }, "SLO warning alert annotations should be valid prometheus keys.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].TicketAlertMeta.Annotations["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].TicketAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", }, "SLO warning alert annotations should have prometheus values.": { - slo: func() prometheus.SLOGroup { + slo: func() model.PromSLOGroup { s := getGoodSLOGroup() s.SLOs[0].TicketAlertMeta.Annotations["something"] = "" return s }, - expErrMessage: "Key: 'SLOGroup.SLOs[0].TicketAlertMeta.Annotations[something]' Error:Field validation for 'Annotations[something]' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Annotations[something]' Error:Field validation for 'Annotations[something]' failed on the 'required' tag", }, } From 2aff6445ea07a066c18fadf5b6f6e1beaf61b0c1 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 29 Mar 2025 09:26:10 +0100 Subject: [PATCH 019/173] Move more to conventions and utils to be importable Signed-off-by: Xabier Larrakoetxea --- internal/prometheus/alert_rules.go | 11 ++-- internal/prometheus/helpers.go | 57 ----------------- internal/prometheus/model.go | 19 ------ internal/prometheus/recording_rules.go | 64 ++++++++++++++----- pkg/common/conventions/sli.go | 13 ++++ .../conventions/{conventions.go => slo.go} | 14 +++- pkg/common/model/slo_prometheus.go | 5 +- pkg/common/utils/prometheus/prometheus.go | 22 +++++++ 8 files changed, 105 insertions(+), 100 deletions(-) delete mode 100644 internal/prometheus/helpers.go create mode 100644 pkg/common/conventions/sli.go rename pkg/common/conventions/{conventions.go => slo.go} (59%) create mode 100644 pkg/common/utils/prometheus/prometheus.go diff --git a/internal/prometheus/alert_rules.go b/internal/prometheus/alert_rules.go index 5e53db82..73281033 100644 --- a/internal/prometheus/alert_rules.go +++ b/internal/prometheus/alert_rules.go @@ -10,6 +10,7 @@ import ( "github.com/slok/sloth/pkg/common/conventions" "github.com/slok/sloth/pkg/common/model" + promutils "github.com/slok/sloth/pkg/common/utils/prometheus" ) // genFunc knows how to generate an SLI recording rule for a specific time window. @@ -51,7 +52,7 @@ func (s sloAlertRulesGenerator) GenerateSLOAlertRules(ctx context.Context, slo S func defaultSLOAlertGenerator(slo SLO, sloAlert AlertMeta, quick, slow model.MWMBAlert) (*rulefmt.Rule, error) { // Generate the filter labels based on the SLO ids. - metricFilter := labelsToPromFilter(getSLOIDPromLabels(slo)) + metricFilter := promutils.LabelsToPromFilter(conventions.GetSLOIDPromLabels(slo)) // Render the alert template. tplData := struct { @@ -69,13 +70,13 @@ func defaultSLOAlertGenerator(slo SLO, sloAlert AlertMeta, quick, slow model.MWM }{ MetricFilter: metricFilter, ErrorBudgetRatio: quick.ErrorBudget / 100, // Any(quick or slow) should work because are the same. - QuickShortMetric: getSLIErrorMetric(quick.ShortWindow), + QuickShortMetric: conventions.GetSLIErrorMetric(quick.ShortWindow), QuickShortBurnFactor: quick.BurnRateFactor, - QuickLongMetric: getSLIErrorMetric(quick.LongWindow), + QuickLongMetric: conventions.GetSLIErrorMetric(quick.LongWindow), QuickLongBurnFactor: quick.BurnRateFactor, - SlowShortMetric: getSLIErrorMetric(slow.ShortWindow), + SlowShortMetric: conventions.GetSLIErrorMetric(slow.ShortWindow), SlowShortBurnFactor: slow.BurnRateFactor, - SlowQuickMetric: getSLIErrorMetric(slow.LongWindow), + SlowQuickMetric: conventions.GetSLIErrorMetric(slow.LongWindow), SlowQuickBurnFactor: slow.BurnRateFactor, WindowLabel: conventions.PromSLOWindowLabelName, } diff --git a/internal/prometheus/helpers.go b/internal/prometheus/helpers.go deleted file mode 100644 index 4e8e8c53..00000000 --- a/internal/prometheus/helpers.go +++ /dev/null @@ -1,57 +0,0 @@ -package prometheus - -import ( - "sort" - "time" - - prommodel "github.com/prometheus/common/model" - "github.com/slok/sloth/pkg/common/model" -) - -func mergeLabels(ms ...map[string]string) map[string]string { - res := map[string]string{} - for _, m := range ms { - for k, v := range m { - res[k] = v - } - } - - return res -} - -func labelsToPromFilter(labels map[string]string) string { - metricFilters := prommodel.LabelSet{} - for k, v := range labels { - metricFilters[prommodel.LabelName(k)] = prommodel.LabelValue(v) - } - - return metricFilters.String() -} - -// Pretty simple durations for prometheus. -func timeDurationToPromStr(t time.Duration) string { - return prommodel.Duration(t).String() -} - -// getAlertGroupWindows gets all the time windows from a multiwindow multiburn alert group. -func getAlertGroupWindows(alerts model.MWMBAlertGroup) []time.Duration { - // Use a map to avoid duplicated windows. - windows := map[string]time.Duration{ - alerts.PageQuick.ShortWindow.String(): alerts.PageQuick.ShortWindow, - alerts.PageQuick.LongWindow.String(): alerts.PageQuick.LongWindow, - alerts.PageSlow.ShortWindow.String(): alerts.PageSlow.ShortWindow, - alerts.PageSlow.LongWindow.String(): alerts.PageSlow.LongWindow, - alerts.TicketQuick.ShortWindow.String(): alerts.TicketQuick.ShortWindow, - alerts.TicketQuick.LongWindow.String(): alerts.TicketQuick.LongWindow, - alerts.TicketSlow.ShortWindow.String(): alerts.TicketSlow.ShortWindow, - alerts.TicketSlow.LongWindow.String(): alerts.TicketSlow.LongWindow, - } - - res := make([]time.Duration, 0, len(windows)) - for _, w := range windows { - res = append(res, w) - } - sort.SliceStable(res, func(i, j int) bool { return res[i] < res[j] }) - - return res -} diff --git a/internal/prometheus/model.go b/internal/prometheus/model.go index 27bd00df..140895d6 100644 --- a/internal/prometheus/model.go +++ b/internal/prometheus/model.go @@ -1,10 +1,6 @@ package prometheus import ( - "fmt" - "time" - - "github.com/slok/sloth/pkg/common/conventions" "github.com/slok/sloth/pkg/common/model" ) @@ -16,18 +12,3 @@ type AlertMeta = model.PromAlertMeta type SLOGroup = model.PromSLOGroup type SLORules = model.PromSLORules type SLIRaw = model.PromSLIRaw - -// getSLIErrorMetric returns the SLI error metric. -func getSLIErrorMetric(window time.Duration) string { - return fmt.Sprintf(conventions.PromSLIErrorMetricFmt, timeDurationToPromStr(window)) -} - -// getSLOIDPromLabels returns the ID labels of an SLO, these can be used to identify -// an SLO recorded metrics and alerts. -func getSLOIDPromLabels(s SLO) map[string]string { - return map[string]string{ - conventions.PromSLOIDLabelName: s.ID, - conventions.PromSLONameLabelName: s.Name, - conventions.PromSLOServiceLabelName: s.Service, - } -} diff --git a/internal/prometheus/recording_rules.go b/internal/prometheus/recording_rules.go index 81582257..dcf92fd4 100644 --- a/internal/prometheus/recording_rules.go +++ b/internal/prometheus/recording_rules.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "fmt" + "maps" + "sort" "strconv" "text/template" "time" @@ -12,6 +14,7 @@ import ( "github.com/slok/sloth/pkg/common/conventions" "github.com/slok/sloth/pkg/common/model" + promutils "github.com/slok/sloth/pkg/common/utils/prometheus" ) // sliRulesgenFunc knows how to generate an SLI recording rule for a specific time window. @@ -83,7 +86,7 @@ func rawSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlert return nil, fmt.Errorf("could not create SLI expression template data: %w", err) } - strWindow := timeDurationToPromStr(window) + strWindow := promutils.TimeDurationToPromStr(window) var b bytes.Buffer err = tpl.Execute(&b, map[string]string{ tplKeyWindow: strWindow, @@ -93,10 +96,10 @@ func rawSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlert } return &rulefmt.Rule{ - Record: getSLIErrorMetric(window), + Record: conventions.GetSLIErrorMetric(window), Expr: b.String(), Labels: mergeLabels( - getSLOIDPromLabels(slo), + conventions.GetSLOIDPromLabels(slo), map[string]string{ conventions.PromSLOWindowLabelName: strWindow, }, @@ -119,7 +122,7 @@ func eventsSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAl return nil, fmt.Errorf("could not create SLI expression template data: %w", err) } - strWindow := timeDurationToPromStr(window) + strWindow := promutils.TimeDurationToPromStr(window) var b bytes.Buffer err = tpl.Execute(&b, map[string]string{ tplKeyWindow: strWindow, @@ -129,10 +132,10 @@ func eventsSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAl } return &rulefmt.Rule{ - Record: getSLIErrorMetric(window), + Record: conventions.GetSLIErrorMetric(window), Expr: b.String(), Labels: mergeLabels( - getSLOIDPromLabels(slo), + conventions.GetSLOIDPromLabels(slo), map[string]string{ conventions.PromSLOWindowLabelName: strWindow, }, @@ -162,8 +165,8 @@ count_over_time({{.metric}}{{.filter}}[{{.window}}]) return nil, fmt.Errorf("can't optimize using the same shortwindow as the window to optimize") } - shortWindowSLIRec := getSLIErrorMetric(shortWindow) - filter := labelsToPromFilter(getSLOIDPromLabels(slo)) + shortWindowSLIRec := conventions.GetSLIErrorMetric(shortWindow) + filter := promutils.LabelsToPromFilter(conventions.GetSLOIDPromLabels(slo)) // Render with our templated data. tpl, err := template.New("sliExpr").Option("missingkey=error").Parse(sliExprTplFmt) @@ -171,7 +174,7 @@ count_over_time({{.metric}}{{.filter}}[{{.window}}]) return nil, fmt.Errorf("could not create SLI expression template data: %w", err) } - strWindow := timeDurationToPromStr(window) + strWindow := promutils.TimeDurationToPromStr(window) var b bytes.Buffer err = tpl.Execute(&b, map[string]string{ "metric": shortWindowSLIRec, @@ -184,10 +187,10 @@ count_over_time({{.metric}}{{.filter}}[{{.window}}]) } return &rulefmt.Rule{ - Record: getSLIErrorMetric(window), + Record: conventions.GetSLIErrorMetric(window), Expr: b.String(), Labels: mergeLabels( - getSLOIDPromLabels(slo), + conventions.GetSLOIDPromLabels(slo), map[string]string{ conventions.PromSLOWindowLabelName: strWindow, }, @@ -203,7 +206,7 @@ type metadataRecordingRulesGenerator bool const MetadataRecordingRulesGenerator = metadataRecordingRulesGenerator(false) func (m metadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx context.Context, info model.Info, slo SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { - labels := mergeLabels(getSLOIDPromLabels(slo), slo.Labels) + labels := mergeLabels(conventions.GetSLOIDPromLabels(slo), slo.Labels) // Metatada Recordings. const ( @@ -218,11 +221,11 @@ func (m metadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx cont sloObjectiveRatio := slo.Objective / 100 - sloFilter := labelsToPromFilter(getSLOIDPromLabels(slo)) + sloFilter := promutils.LabelsToPromFilter(conventions.GetSLOIDPromLabels(slo)) var currentBurnRateExpr bytes.Buffer err := burnRateRecordingExprTpl.Execute(¤tBurnRateExpr, map[string]string{ - "SLIErrorMetric": getSLIErrorMetric(alerts.PageQuick.ShortWindow), + "SLIErrorMetric": conventions.GetSLIErrorMetric(alerts.PageQuick.ShortWindow), "MetricFilter": sloFilter, "SLOIDName": conventions.PromSLOIDLabelName, "SLOLabelName": conventions.PromSLONameLabelName, @@ -235,7 +238,7 @@ func (m metadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx cont var periodBurnRateExpr bytes.Buffer err = burnRateRecordingExprTpl.Execute(&periodBurnRateExpr, map[string]string{ - "SLIErrorMetric": getSLIErrorMetric(slo.TimeWindow), + "SLIErrorMetric": conventions.GetSLIErrorMetric(slo.TimeWindow), "MetricFilter": sloFilter, "SLOIDName": conventions.PromSLOIDLabelName, "SLOLabelName": conventions.PromSLONameLabelName, @@ -309,3 +312,34 @@ var burnRateRecordingExprTpl = template.Must(template.New("burnRateExpr").Option / on({{ .SLOIDName }}, {{ .SLOLabelName }}, {{ .SLOServiceName }}) group_left {{ .ErrorBudgetRatioMetric }}{{ .MetricFilter }} `)) + +// getAlertGroupWindows gets all the time windows from a multiwindow multiburn alert group. +func getAlertGroupWindows(alerts model.MWMBAlertGroup) []time.Duration { + // Use a map to avoid duplicated windows. + windows := map[string]time.Duration{ + alerts.PageQuick.ShortWindow.String(): alerts.PageQuick.ShortWindow, + alerts.PageQuick.LongWindow.String(): alerts.PageQuick.LongWindow, + alerts.PageSlow.ShortWindow.String(): alerts.PageSlow.ShortWindow, + alerts.PageSlow.LongWindow.String(): alerts.PageSlow.LongWindow, + alerts.TicketQuick.ShortWindow.String(): alerts.TicketQuick.ShortWindow, + alerts.TicketQuick.LongWindow.String(): alerts.TicketQuick.LongWindow, + alerts.TicketSlow.ShortWindow.String(): alerts.TicketSlow.ShortWindow, + alerts.TicketSlow.LongWindow.String(): alerts.TicketSlow.LongWindow, + } + + res := make([]time.Duration, 0, len(windows)) + for _, w := range windows { + res = append(res, w) + } + sort.SliceStable(res, func(i, j int) bool { return res[i] < res[j] }) + + return res +} + +func mergeLabels[M ~map[K]V, K comparable, V any](ms ...M) M { + m := make(M) + for _, m2 := range ms { + maps.Copy(m, m2) + } + return m +} diff --git a/pkg/common/conventions/sli.go b/pkg/common/conventions/sli.go new file mode 100644 index 00000000..8f588981 --- /dev/null +++ b/pkg/common/conventions/sli.go @@ -0,0 +1,13 @@ +package conventions + +import ( + "fmt" + "time" + + promutils "github.com/slok/sloth/pkg/common/utils/prometheus" +) + +// GetSLIErrorMetric returns the SLI error Prometheus metric name. +func GetSLIErrorMetric(window time.Duration) string { + return fmt.Sprintf(PromSLIErrorMetricFmt, promutils.TimeDurationToPromStr(window)) +} diff --git a/pkg/common/conventions/conventions.go b/pkg/common/conventions/slo.go similarity index 59% rename from pkg/common/conventions/conventions.go rename to pkg/common/conventions/slo.go index a3c8becc..03b88c64 100644 --- a/pkg/common/conventions/conventions.go +++ b/pkg/common/conventions/slo.go @@ -1,5 +1,7 @@ package conventions +import "github.com/slok/sloth/pkg/common/model" + // Prometheus metrics conventions. const ( // Metrics. @@ -15,6 +17,14 @@ const ( PromSLOModeLabelName = "sloth_mode" PromSLOSpecLabelName = "sloth_spec" PromSLOObjectiveLabelName = "sloth_objective" - - PromQueryTPLKeyWindow = "window" ) + +// GetSLOIDPromLabels returns the ID labels of an SLO, these can be used to identify +// an SLO recorded metrics and alerts. +func GetSLOIDPromLabels(s model.PromSLO) map[string]string { + return map[string]string{ + PromSLOIDLabelName: s.ID, + PromSLONameLabelName: s.Name, + PromSLOServiceLabelName: s.Service, + } +} diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index b9236df6..704ce7cb 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -12,7 +12,6 @@ import ( prommodel "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/rulefmt" promqlparser "github.com/prometheus/prometheus/promql/parser" - "github.com/slok/sloth/pkg/common/conventions" ) // SLI represents an SLI with custom error and total expressions. @@ -180,7 +179,9 @@ func validateRequiredEnabledAlertName(fl validator.FieldLevel) bool { return alertMeta.Name != "" } -var tplWindowRegex = regexp.MustCompile(fmt.Sprintf(`{{ *\.%s *}}`, conventions.PromQueryTPLKeyWindow)) +const PromQueryTPLKeyWindow = "window" + +var tplWindowRegex = regexp.MustCompile(fmt.Sprintf(`{{ *\.%s *}}`, PromQueryTPLKeyWindow)) // validateTemplateVars implements validator.CustomTypeFunc by validating // an SLI template has all the required fields. diff --git a/pkg/common/utils/prometheus/prometheus.go b/pkg/common/utils/prometheus/prometheus.go new file mode 100644 index 00000000..465799ee --- /dev/null +++ b/pkg/common/utils/prometheus/prometheus.go @@ -0,0 +1,22 @@ +package prometheus + +import ( + "time" + + prommodel "github.com/prometheus/common/model" +) + +// TimeDurationToPromStr converts from std duration to prom string duration. +func TimeDurationToPromStr(t time.Duration) string { + return prommodel.Duration(t).String() +} + +// LabelsToPromFilter converts a map of labels to a Prometheus filter string. +func LabelsToPromFilter(labels map[string]string) string { + metricFilters := prommodel.LabelSet{} + for k, v := range labels { + metricFilters[prommodel.LabelName(k)] = prommodel.LabelValue(v) + } + + return metricFilters.String() +} From 29114efde99b379cadcb364d1c5dd7e283bb73f6 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 29 Mar 2025 10:26:29 +0100 Subject: [PATCH 020/173] Move sli plugins loader and reader to independent packages Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/helpers.go | 14 +- internal/k8sprometheus/spec.go | 3 +- internal/k8sprometheus/spec_test.go | 13 +- internal/plugin/sli/sli.go | 101 ++++++++++++ .../sli/sli_test.go} | 29 +--- internal/prometheus/spec.go | 3 +- internal/prometheus/spec_test.go | 13 +- .../fs/fsmock}/file_manager.go | 2 +- .../storage/fs/fsmock/sli_plugin_loader.go | 60 +++++++ .../{prometheus => storage/fs}/sli_plugin.go | 126 +++------------ internal/storage/fs/sli_plugin_test.go | 152 ++++++++++++++++++ 11 files changed, 371 insertions(+), 145 deletions(-) create mode 100644 internal/plugin/sli/sli.go rename internal/{prometheus/sli_plugin_test.go => plugin/sli/sli_test.go} (77%) rename internal/{prometheus/prometheusmock => storage/fs/fsmock}/file_manager.go (98%) create mode 100644 internal/storage/fs/fsmock/sli_plugin_loader.go rename internal/{prometheus => storage/fs}/sli_plugin.go (56%) create mode 100644 internal/storage/fs/sli_plugin_test.go diff --git a/cmd/sloth/commands/helpers.go b/cmd/sloth/commands/helpers.go index 817caa6e..f2f99e60 100644 --- a/cmd/sloth/commands/helpers.go +++ b/cmd/sloth/commands/helpers.go @@ -10,7 +10,8 @@ import ( "strings" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/prometheus" + pluginsli "github.com/slok/sloth/internal/plugin/sli" + storagefs "github.com/slok/sloth/internal/storage/fs" ) var ( @@ -38,12 +39,13 @@ func splitYAML(data []byte) []string { return nonEmptyData } -func createPluginLoader(ctx context.Context, logger log.Logger, paths []string) (*prometheus.FileSLIPluginRepo, error) { - config := prometheus.FileSLIPluginRepoConfig{ - Paths: paths, - Logger: logger, +func createPluginLoader(ctx context.Context, logger log.Logger, paths []string) (*storagefs.FileSLIPluginRepo, error) { + config := storagefs.FileSLIPluginRepoConfig{ + Paths: paths, + PluginLoader: pluginsli.PluginLoader, + Logger: logger, } - sliPluginRepo, err := prometheus.NewFileSLIPluginRepo(config) + sliPluginRepo, err := storagefs.NewFileSLIPluginRepo(config) if err != nil { return nil, fmt.Errorf("could not create file SLI plugin repository: %w", err) } diff --git a/internal/k8sprometheus/spec.go b/internal/k8sprometheus/spec.go index d2226435..2f8bdec7 100644 --- a/internal/k8sprometheus/spec.go +++ b/internal/k8sprometheus/spec.go @@ -8,6 +8,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" + pluginsli "github.com/slok/sloth/internal/plugin/sli" "github.com/slok/sloth/internal/prometheus" k8sprometheusv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/scheme" @@ -15,7 +16,7 @@ import ( ) type SLIPluginRepo interface { - GetSLIPlugin(ctx context.Context, id string) (*prometheus.SLIPlugin, error) + GetSLIPlugin(ctx context.Context, id string) (*pluginsli.SLIPlugin, error) } // YAMLSpecLoader knows how to load Kubernetes ServiceLevel YAML specs and converts them to a model. diff --git a/internal/k8sprometheus/spec_test.go b/internal/k8sprometheus/spec_test.go index 5734c524..8db0ea62 100644 --- a/internal/k8sprometheus/spec_test.go +++ b/internal/k8sprometheus/spec_test.go @@ -9,12 +9,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/slok/sloth/internal/k8sprometheus" + pluginsli "github.com/slok/sloth/internal/plugin/sli" "github.com/slok/sloth/internal/prometheus" ) -type testMemPluginsRepo map[string]prometheus.SLIPlugin +type testMemPluginsRepo map[string]pluginsli.SLIPlugin -func (t testMemPluginsRepo) GetSLIPlugin(ctx context.Context, id string) (*prometheus.SLIPlugin, error) { +func (t testMemPluginsRepo) GetSLIPlugin(ctx context.Context, id string) (*pluginsli.SLIPlugin, error) { p, ok := t[id] if !ok { return nil, fmt.Errorf("unknown plugin") @@ -25,7 +26,7 @@ func (t testMemPluginsRepo) GetSLIPlugin(ctx context.Context, id string) (*prome func TestYAMLoadSpec(t *testing.T) { tests := map[string]struct { specYaml string - plugins map[string]prometheus.SLIPlugin + plugins map[string]pluginsli.SLIPlugin expModel *k8sprometheus.SLOGroup expErr bool }{ @@ -114,7 +115,7 @@ spec: }, "Spec with SLI plugin should use the plugin correctly.": { - plugins: map[string]prometheus.SLIPlugin{ + plugins: map[string]pluginsli.SLIPlugin{ "test_plugin": { ID: "test_plugin", Func: func(ctx context.Context, meta map[string]string, labels map[string]string, options map[string]string) (string, error) { @@ -182,7 +183,7 @@ spec: }, "An spec with SLI plugin that returns an error should use the plugin correctly and fail.": { - plugins: map[string]prometheus.SLIPlugin{ + plugins: map[string]pluginsli.SLIPlugin{ "test_plugin": { ID: "test_plugin", Func: func(ctx context.Context, meta map[string]string, labels map[string]string, options map[string]string) (string, error) { @@ -434,7 +435,7 @@ kind: PrometheusServiceLevel t.Run(name, func(t *testing.T) { assert := assert.New(t) - loader := k8sprometheus.NewYAMLSpecLoader(testMemPluginsRepo(map[string]prometheus.SLIPlugin{}), 30*24*time.Hour) + loader := k8sprometheus.NewYAMLSpecLoader(testMemPluginsRepo(map[string]pluginsli.SLIPlugin{}), 30*24*time.Hour) got := loader.IsSpecType(context.TODO(), []byte(test.specYaml)) assert.Equal(test.exp, got) diff --git a/internal/plugin/sli/sli.go b/internal/plugin/sli/sli.go new file mode 100644 index 00000000..1cd9f923 --- /dev/null +++ b/internal/plugin/sli/sli.go @@ -0,0 +1,101 @@ +package sli + +import ( + "context" + "fmt" + "regexp" + + "github.com/traefik/yaegi/interp" + "github.com/traefik/yaegi/stdlib" + + pluginv1 "github.com/slok/sloth/pkg/prometheus/plugin/v1" +) + +type SLIPlugin struct { + ID string + Func pluginv1.SLIPlugin +} + +// PluginLoader knows how to load Go SLI plugins using Yaegi. +const PluginLoader = sliPluginLoader(false) + +type sliPluginLoader bool + +var packageRegexp = regexp.MustCompile(`(?m)^package +([^\s]+) *$`) + +// LoadRawSLIPlugin knows how to load plugins using Yaegi from source data not files, +// thats why, this implementation will not support any import library except standard +// library. +// +// The load process will search for: +// - A function called `SLIPlugin` to obtain the plugin func. +// - A constant called `SLIPluginID` to obtain the plugin ID. +// - A constant called `SLIPluginVersion` to obtain the plugin version. +func (s sliPluginLoader) LoadRawSLIPlugin(ctx context.Context, src string) (*SLIPlugin, error) { + // Load the plugin in a new interpreter. + // For each plugin we need to use an independent interpreter to avoid name collisions. + yaegiInterp, err := s.newYaeginInterpreter() + if err != nil { + return nil, fmt.Errorf("could not create a new Yaegi interpreter: %w", err) + } + + _, err = yaegiInterp.EvalWithContext(ctx, src) + if err != nil { + return nil, fmt.Errorf("could not evaluate plugin source code: %w", err) + } + + // Discover package name. + packageMatch := packageRegexp.FindStringSubmatch(src) + if len(packageMatch) != 2 { + return nil, fmt.Errorf("invalid plugin source code, could not get package name") + } + packageName := packageMatch[1] + + // Get plugin version and check if is a known one. + pluginVerTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.SLIPluginVersion", packageName)) + if err != nil { + return nil, fmt.Errorf("could not get plugin version: %w", err) + } + + pluginVer, ok := pluginVerTmp.Interface().(pluginv1.SLIPluginVersion) + if !ok || (pluginVer != pluginv1.Version) { + return nil, fmt.Errorf("unsuported plugin version: %s", pluginVer) + } + + // Get plugin ID. + pluginIDTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.SLIPluginID", packageName)) + if err != nil { + return nil, fmt.Errorf("could not get plugin ID: %w", err) + } + + pluginID, ok := pluginIDTmp.Interface().(pluginv1.SLIPluginID) + if !ok { + return nil, fmt.Errorf("invalid SLI plugin ID type") + } + + // Get plugin logic. + pluginFuncTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.SLIPlugin", packageName)) + if err != nil { + return nil, fmt.Errorf("could not get plugin: %w", err) + } + + pluginFunc, ok := pluginFuncTmp.Interface().(pluginv1.SLIPlugin) + if !ok { + return nil, fmt.Errorf("invalid SLI plugin type") + } + + return &SLIPlugin{ + ID: pluginID, + Func: pluginFunc, + }, nil +} + +func (s sliPluginLoader) newYaeginInterpreter() (*interp.Interpreter, error) { + i := interp.New(interp.Options{}) + err := i.Use(stdlib.Symbols) + if err != nil { + return nil, fmt.Errorf("could not use stdlib symbols: %w", err) + } + + return i, nil +} diff --git a/internal/prometheus/sli_plugin_test.go b/internal/plugin/sli/sli_test.go similarity index 77% rename from internal/prometheus/sli_plugin_test.go rename to internal/plugin/sli/sli_test.go index f7f6bb2a..be9e0c45 100644 --- a/internal/prometheus/sli_plugin_test.go +++ b/internal/plugin/sli/sli_test.go @@ -1,21 +1,18 @@ -package prometheus_test +package sli_test import ( "context" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/slok/sloth/internal/prometheus" - "github.com/slok/sloth/internal/prometheus/prometheusmock" + "github.com/slok/sloth/internal/plugin/sli" ) func TestSLIPluginLoader(t *testing.T) { tests := map[string]struct { pluginSrc string - pluginID string meta map[string]string labels map[string]string options map[string]string @@ -113,26 +110,14 @@ func SLIPlugin(ctx context.Context, meta, labels, options map[string]string) (st assert := assert.New(t) require := require.New(t) - // Mock the plugin files. - mfm := &prometheusmock.FileManager{} - mfm.On("FindFiles", mock.Anything, "./", mock.Anything).Once().Return([]string{"testplugin/test.go"}, nil) - mfm.On("ReadFile", mock.Anything, "testplugin/test.go").Once().Return([]byte(test.pluginSrc), nil) - - // Create repository and load plugins. - config := prometheus.FileSLIPluginRepoConfig{ - FileManager: mfm, - Paths: []string{"./"}, - } - repo, err := prometheus.NewFileSLIPluginRepo(config) + // Get plugin. + plugin, err := sli.PluginLoader.LoadRawSLIPlugin(context.Background(), test.pluginSrc) if test.expErrLoad { - assert.Error(err) + require.Error(err) return + } else { + assert.NoError(err) } - assert.NoError(err) - - // Get plugin. - plugin, err := repo.GetSLIPlugin(context.TODO(), test.expPluginID) - require.NoError(err) // Check. assert.Equal(test.expPluginID, plugin.ID) diff --git a/internal/prometheus/spec.go b/internal/prometheus/spec.go index 9db23bd2..2fdab87a 100644 --- a/internal/prometheus/spec.go +++ b/internal/prometheus/spec.go @@ -8,12 +8,13 @@ import ( "gopkg.in/yaml.v2" + pluginsli "github.com/slok/sloth/internal/plugin/sli" prometheusv1 "github.com/slok/sloth/pkg/prometheus/api/v1" prometheuspluginv1 "github.com/slok/sloth/pkg/prometheus/plugin/v1" ) type SLIPluginRepo interface { - GetSLIPlugin(ctx context.Context, id string) (*SLIPlugin, error) + GetSLIPlugin(ctx context.Context, id string) (*pluginsli.SLIPlugin, error) } // YAMLSpecLoader knows how to load YAML specs and converts them to a model. diff --git a/internal/prometheus/spec_test.go b/internal/prometheus/spec_test.go index 8c33a4fd..4a1d46b0 100644 --- a/internal/prometheus/spec_test.go +++ b/internal/prometheus/spec_test.go @@ -8,12 +8,13 @@ import ( "github.com/stretchr/testify/assert" + pluginsli "github.com/slok/sloth/internal/plugin/sli" "github.com/slok/sloth/internal/prometheus" ) -type testMemPluginsRepo map[string]prometheus.SLIPlugin +type testMemPluginsRepo map[string]pluginsli.SLIPlugin -func (t testMemPluginsRepo) GetSLIPlugin(ctx context.Context, id string) (*prometheus.SLIPlugin, error) { +func (t testMemPluginsRepo) GetSLIPlugin(ctx context.Context, id string) (*pluginsli.SLIPlugin, error) { p, ok := t[id] if !ok { return nil, fmt.Errorf("unknown plugin") @@ -24,7 +25,7 @@ func (t testMemPluginsRepo) GetSLIPlugin(ctx context.Context, id string) (*prome func TestYAMLoadSpec(t *testing.T) { tests := map[string]struct { specYaml string - plugins map[string]prometheus.SLIPlugin + plugins map[string]pluginsli.SLIPlugin windowPeriod time.Duration expModel *prometheus.SLOGroup expErr bool @@ -87,7 +88,7 @@ slos: }, "Spec with SLI plugin that returns an error should use the plugin correctly and fail.": { - plugins: map[string]prometheus.SLIPlugin{ + plugins: map[string]pluginsli.SLIPlugin{ "test_plugin": { ID: "test_plugin", Func: func(ctx context.Context, meta map[string]string, labels map[string]string, options map[string]string) (string, error) { @@ -115,7 +116,7 @@ slos: "Spec with SLI plugin should use the plugin correctly.": { windowPeriod: 30 * 24 * time.Hour, - plugins: map[string]prometheus.SLIPlugin{ + plugins: map[string]pluginsli.SLIPlugin{ "test_plugin": { ID: "test_plugin", Func: func(ctx context.Context, meta map[string]string, labels map[string]string, options map[string]string) (string, error) { @@ -382,7 +383,7 @@ func TestYAMLIsSpecType(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - loader := prometheus.NewYAMLSpecLoader(testMemPluginsRepo(map[string]prometheus.SLIPlugin{}), 0) + loader := prometheus.NewYAMLSpecLoader(testMemPluginsRepo(map[string]pluginsli.SLIPlugin{}), 0) got := loader.IsSpecType(context.TODO(), []byte(test.specYaml)) assert.Equal(test.exp, got) diff --git a/internal/prometheus/prometheusmock/file_manager.go b/internal/storage/fs/fsmock/file_manager.go similarity index 98% rename from internal/prometheus/prometheusmock/file_manager.go rename to internal/storage/fs/fsmock/file_manager.go index 2b0f2d2a..0cc40d54 100644 --- a/internal/prometheus/prometheusmock/file_manager.go +++ b/internal/storage/fs/fsmock/file_manager.go @@ -1,6 +1,6 @@ // Code generated by mockery v2.53.3. DO NOT EDIT. -package prometheusmock +package fsmock import ( context "context" diff --git a/internal/storage/fs/fsmock/sli_plugin_loader.go b/internal/storage/fs/fsmock/sli_plugin_loader.go new file mode 100644 index 00000000..84528ad4 --- /dev/null +++ b/internal/storage/fs/fsmock/sli_plugin_loader.go @@ -0,0 +1,60 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package fsmock + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + sli "github.com/slok/sloth/internal/plugin/sli" +) + +// SLIPluginLoader is an autogenerated mock type for the SLIPluginLoader type +type SLIPluginLoader struct { + mock.Mock +} + +// LoadRawSLIPlugin provides a mock function with given fields: ctx, src +func (_m *SLIPluginLoader) LoadRawSLIPlugin(ctx context.Context, src string) (*sli.SLIPlugin, error) { + ret := _m.Called(ctx, src) + + if len(ret) == 0 { + panic("no return value specified for LoadRawSLIPlugin") + } + + var r0 *sli.SLIPlugin + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*sli.SLIPlugin, error)); ok { + return rf(ctx, src) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *sli.SLIPlugin); ok { + r0 = rf(ctx, src) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sli.SLIPlugin) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, src) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewSLIPluginLoader creates a new instance of SLIPluginLoader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSLIPluginLoader(t interface { + mock.TestingT + Cleanup(func()) +}) *SLIPluginLoader { + mock := &SLIPluginLoader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/prometheus/sli_plugin.go b/internal/storage/fs/sli_plugin.go similarity index 56% rename from internal/prometheus/sli_plugin.go rename to internal/storage/fs/sli_plugin.go index 98b3a2a4..c9f6ff10 100644 --- a/internal/prometheus/sli_plugin.go +++ b/internal/storage/fs/sli_plugin.go @@ -1,4 +1,4 @@ -package prometheus +package fs import ( "context" @@ -9,20 +9,24 @@ import ( "regexp" "sync" - "github.com/traefik/yaegi/interp" - "github.com/traefik/yaegi/stdlib" - "github.com/slok/sloth/internal/log" - pluginv1 "github.com/slok/sloth/pkg/prometheus/plugin/v1" + pluginsli "github.com/slok/sloth/internal/plugin/sli" ) +type SLIPluginLoader interface { + LoadRawSLIPlugin(ctx context.Context, src string) (*pluginsli.SLIPlugin, error) +} + +//go:generate mockery --case underscore --output fsmock --outpkg fsmock --name SLIPluginLoader + // FileManager knows how to manage files. +// TODO(slok): Use fs.FS. type FileManager interface { FindFiles(ctx context.Context, root string, matcher *regexp.Regexp) (paths []string, err error) ReadFile(ctx context.Context, path string) (data []byte, err error) } -//go:generate mockery --case underscore --output prometheusmock --outpkg prometheusmock --name FileManager +//go:generate mockery --case underscore --output fsmock --outpkg fsmock --name FileManager type fileManager struct{} @@ -55,15 +59,11 @@ func (f fileManager) ReadFile(_ context.Context, path string) ([]byte, error) { return os.ReadFile(path) } -type SLIPlugin struct { - ID string - Func pluginv1.SLIPlugin -} - type FileSLIPluginRepoConfig struct { - FileManager FileManager - Paths []string - Logger log.Logger + FileManager FileManager + Paths []string + PluginLoader SLIPluginLoader + Logger log.Logger } func (c *FileSLIPluginRepoConfig) defaults() error { @@ -71,6 +71,10 @@ func (c *FileSLIPluginRepoConfig) defaults() error { c.FileManager = fileManager{} } + if c.PluginLoader == nil { + c.PluginLoader = pluginsli.PluginLoader + } + if c.Logger == nil { c.Logger = log.Noop } @@ -87,7 +91,7 @@ func NewFileSLIPluginRepo(config FileSLIPluginRepoConfig) (*FileSLIPluginRepo, e f := &FileSLIPluginRepo{ fileManager: config.FileManager, - pluginLoader: sliPluginLoader{}, + pluginLoader: config.PluginLoader, paths: config.Paths, logger: config.Logger, } @@ -115,10 +119,10 @@ func NewFileSLIPluginRepo(config FileSLIPluginRepoConfig) (*FileSLIPluginRepo, e // - Force keeping the plugins simple, small and without smart code. // - Force avoiding DRY in small plugins and embrace WET to have independent plugins. type FileSLIPluginRepo struct { - pluginLoader sliPluginLoader + pluginLoader SLIPluginLoader fileManager FileManager paths []string - plugins map[string]SLIPlugin + plugins map[string]pluginsli.SLIPlugin mu sync.RWMutex logger log.Logger } @@ -140,7 +144,7 @@ func (f *FileSLIPluginRepo) Reload(ctx context.Context) error { } // Load the plugins. - plugins := map[string]SLIPlugin{} + plugins := map[string]pluginsli.SLIPlugin{} for path := range paths { pluginData, err := f.fileManager.ReadFile(ctx, path) if err != nil { @@ -173,14 +177,14 @@ func (f *FileSLIPluginRepo) Reload(ctx context.Context) error { return nil } -func (f *FileSLIPluginRepo) ListSLIPlugins(ctx context.Context) (map[string]SLIPlugin, error) { +func (f *FileSLIPluginRepo) ListSLIPlugins(ctx context.Context) (map[string]pluginsli.SLIPlugin, error) { f.mu.RLock() defer f.mu.RUnlock() return f.plugins, nil } -func (f *FileSLIPluginRepo) GetSLIPlugin(ctx context.Context, id string) (*SLIPlugin, error) { +func (f *FileSLIPluginRepo) GetSLIPlugin(ctx context.Context, id string) (*pluginsli.SLIPlugin, error) { f.mu.RLock() defer f.mu.RUnlock() @@ -191,85 +195,3 @@ func (f *FileSLIPluginRepo) GetSLIPlugin(ctx context.Context, id string) (*SLIPl return &p, nil } - -// sliPluginLoader knows how to load Go SLI plugins using Yaegi. -type sliPluginLoader struct{} - -var packageRegexp = regexp.MustCompile(`(?m)^package +([^\s]+) *$`) - -// LoadRawSLIPlugin knows how to load plugins using Yaegi from source data not files, -// thats why, this implementation will not support any import library except standard -// library. -// -// The load process will search for: -// - A function called `SLIPlugin` to obtain the plugin func. -// - A constant called `SLIPluginID` to obtain the plugin ID. -// - A constant called `SLIPluginVersion` to obtain the plugin version. -func (s sliPluginLoader) LoadRawSLIPlugin(ctx context.Context, src string) (*SLIPlugin, error) { - // Load the plugin in a new interpreter. - // For each plugin we need to use an independent interpreter to avoid name collisions. - yaegiInterp, err := s.newYaeginInterpreter() - if err != nil { - return nil, fmt.Errorf("could not create a new Yaegi interpreter: %w", err) - } - - _, err = yaegiInterp.EvalWithContext(ctx, src) - if err != nil { - return nil, fmt.Errorf("could not evaluate plugin source code: %w", err) - } - - // Discover package name. - packageMatch := packageRegexp.FindStringSubmatch(src) - if len(packageMatch) != 2 { - return nil, fmt.Errorf("invalid plugin source code, could not get package name") - } - packageName := packageMatch[1] - - // Get plugin version and check if is a known one. - pluginVerTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.SLIPluginVersion", packageName)) - if err != nil { - return nil, fmt.Errorf("could not get plugin version: %w", err) - } - - pluginVer, ok := pluginVerTmp.Interface().(pluginv1.SLIPluginVersion) - if !ok || (pluginVer != pluginv1.Version) { - return nil, fmt.Errorf("unsuported plugin version: %s", pluginVer) - } - - // Get plugin ID. - pluginIDTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.SLIPluginID", packageName)) - if err != nil { - return nil, fmt.Errorf("could not get plugin ID: %w", err) - } - - pluginID, ok := pluginIDTmp.Interface().(pluginv1.SLIPluginID) - if !ok { - return nil, fmt.Errorf("invalid SLI plugin ID type") - } - - // Get plugin logic. - pluginFuncTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.SLIPlugin", packageName)) - if err != nil { - return nil, fmt.Errorf("could not get plugin: %w", err) - } - - pluginFunc, ok := pluginFuncTmp.Interface().(pluginv1.SLIPlugin) - if !ok { - return nil, fmt.Errorf("invalid SLI plugin type") - } - - return &SLIPlugin{ - ID: pluginID, - Func: pluginFunc, - }, nil -} - -func (s sliPluginLoader) newYaeginInterpreter() (*interp.Interpreter, error) { - i := interp.New(interp.Options{}) - err := i.Use(stdlib.Symbols) - if err != nil { - return nil, fmt.Errorf("could not use stdlib symbols: %w", err) - } - - return i, nil -} diff --git a/internal/storage/fs/sli_plugin_test.go b/internal/storage/fs/sli_plugin_test.go new file mode 100644 index 00000000..db5f1851 --- /dev/null +++ b/internal/storage/fs/sli_plugin_test.go @@ -0,0 +1,152 @@ +package fs_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + pluginsli "github.com/slok/sloth/internal/plugin/sli" + "github.com/slok/sloth/internal/storage/fs" + "github.com/slok/sloth/internal/storage/fs/fsmock" +) + +func TestSLIPluginRepoListSLIPlugins(t *testing.T) { + tests := map[string]struct { + mock func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) + expPlugins map[string]pluginsli.SLIPlugin + expErr bool + }{ + "Having no files, should return empty list of plugins.": { + mock: func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) { + mfm.On("FindFiles", mock.Anything, "./", mock.Anything).Once().Return([]string{}, nil) + }, + expPlugins: map[string]pluginsli.SLIPlugin{}, + }, + + "Having multiple files, should return multiple plugins.": { + mock: func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) { + mfm.On("FindFiles", mock.Anything, "./", mock.Anything).Once().Return([]string{ + "./test_plugin_1.go", + "./test_plugin_2.go", + "./test2/test_plugin_3.go", + "./test3/test4/test_plugin_4.go", + }, nil) + + mfm.On("ReadFile", mock.Anything, "./test_plugin_1.go").Once().Return([]byte(`test1`), nil) + mfm.On("ReadFile", mock.Anything, "./test_plugin_2.go").Once().Return([]byte(`test2`), nil) + mfm.On("ReadFile", mock.Anything, "./test2/test_plugin_3.go").Once().Return([]byte(`test3`), nil) + mfm.On("ReadFile", mock.Anything, "./test3/test4/test_plugin_4.go").Once().Return([]byte(`test4`), nil) + + mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginsli.SLIPlugin{ID: "test1"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginsli.SLIPlugin{ID: "test2"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test3").Once().Return(&pluginsli.SLIPlugin{ID: "test3"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test4").Once().Return(&pluginsli.SLIPlugin{ID: "test4"}, nil) + }, + expPlugins: map[string]pluginsli.SLIPlugin{ + "test1": {ID: "test1"}, + "test2": {ID: "test2"}, + "test3": {ID: "test3"}, + "test4": {ID: "test4"}, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + mfm := fsmock.NewFileManager(t) + mpl := fsmock.NewSLIPluginLoader(t) + test.mock(mfm, mpl) + + // Create repository and load plugins. + config := fs.FileSLIPluginRepoConfig{ + FileManager: mfm, + PluginLoader: mpl, + Paths: []string{"./"}, + } + repo, err := fs.NewFileSLIPluginRepo(config) + require.NoError(err) + + plugins, err := repo.ListSLIPlugins(t.Context()) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expPlugins, plugins) + } + }) + } +} + +func TestSLIPluginRepoGetSLIPlugin(t *testing.T) { + tests := map[string]struct { + pluginID string + mock func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) + expPlugin pluginsli.SLIPlugin + expErr bool + }{ + "Having a missing plugin, should fail.": { + pluginID: "test3", + mock: func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) { + mfm.On("FindFiles", mock.Anything, "./", mock.Anything).Once().Return([]string{ + "./test_plugin_1.go", + "./test_plugin_2.go", + }, nil) + + mfm.On("ReadFile", mock.Anything, "./test_plugin_1.go").Once().Return([]byte(`test1`), nil) + mfm.On("ReadFile", mock.Anything, "./test_plugin_2.go").Once().Return([]byte(`test2`), nil) + + mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginsli.SLIPlugin{ID: "test1"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginsli.SLIPlugin{ID: "test2"}, nil) + }, + expErr: true, + }, + + "Having a correct plugin, should return the plugin.": { + pluginID: "test2", + mock: func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) { + mfm.On("FindFiles", mock.Anything, "./", mock.Anything).Once().Return([]string{ + "./test_plugin_1.go", + "./test_plugin_2.go", + }, nil) + + mfm.On("ReadFile", mock.Anything, "./test_plugin_1.go").Once().Return([]byte(`test1`), nil) + mfm.On("ReadFile", mock.Anything, "./test_plugin_2.go").Once().Return([]byte(`test2`), nil) + + mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginsli.SLIPlugin{ID: "test1"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginsli.SLIPlugin{ID: "test2"}, nil) + }, + expPlugin: pluginsli.SLIPlugin{ID: "test2"}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + mfm := fsmock.NewFileManager(t) + mpl := fsmock.NewSLIPluginLoader(t) + test.mock(mfm, mpl) + + // Create repository and load plugins. + config := fs.FileSLIPluginRepoConfig{ + FileManager: mfm, + PluginLoader: mpl, + Paths: []string{"./"}, + } + repo, err := fs.NewFileSLIPluginRepo(config) + require.NoError(err) + + plugin, err := repo.GetSLIPlugin(t.Context(), test.pluginID) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expPlugin, *plugin) + } + }) + } +} From 26675b6c99e736139f7a136c78c05d5516518e15 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 29 Mar 2025 11:07:30 +0100 Subject: [PATCH 021/173] Move prom SLO IO writer to storage package Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 13 +++--- .../io/slo_prometheus.go} | 23 +++++----- .../io/slo_prometheus_test.go} | 43 ++++++++++--------- 3 files changed, 41 insertions(+), 38 deletions(-) rename internal/{prometheus/storage.go => storage/io/slo_prometheus.go} (83%) rename internal/{prometheus/storage_test.go => storage/io/slo_prometheus_test.go} (87%) diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 5388deb6..def71e7f 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -23,6 +23,7 @@ import ( "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/openslo" "github.com/slok/sloth/internal/prometheus" + storageio "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" kubernetesv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" prometheusv1 "github.com/slok/sloth/pkg/prometheus/api/v1" @@ -322,10 +323,10 @@ func (g generator) GeneratePrometheus(ctx context.Context, slos prometheus.SLOGr return err } - repo := prometheus.NewIOWriterGroupedRulesYAMLRepo(out, g.logger) - storageSLOs := make([]prometheus.StorageSLO, 0, len(result.PrometheusSLOs)) + repo := storageio.NewGroupedRulesYAMLRepo(out, g.logger) + storageSLOs := make([]storageio.StorageSLO, 0, len(result.PrometheusSLOs)) for _, s := range result.PrometheusSLOs { - storageSLOs = append(storageSLOs, prometheus.StorageSLO{ + storageSLOs = append(storageSLOs, storageio.StorageSLO{ SLO: s.SLO, Rules: s.SLORules, }) @@ -384,10 +385,10 @@ func (g generator) GenerateOpenSLO(ctx context.Context, slos prometheus.SLOGroup return err } - repo := prometheus.NewIOWriterGroupedRulesYAMLRepo(out, g.logger) - storageSLOs := make([]prometheus.StorageSLO, 0, len(result.PrometheusSLOs)) + repo := storageio.NewGroupedRulesYAMLRepo(out, g.logger) + storageSLOs := make([]storageio.StorageSLO, 0, len(result.PrometheusSLOs)) for _, s := range result.PrometheusSLOs { - storageSLOs = append(storageSLOs, prometheus.StorageSLO{ + storageSLOs = append(storageSLOs, storageio.StorageSLO{ SLO: s.SLO, Rules: s.SLORules, }) diff --git a/internal/prometheus/storage.go b/internal/storage/io/slo_prometheus.go similarity index 83% rename from internal/prometheus/storage.go rename to internal/storage/io/slo_prometheus.go index 9b37ca92..c0cd5340 100644 --- a/internal/prometheus/storage.go +++ b/internal/storage/io/slo_prometheus.go @@ -1,4 +1,4 @@ -package prometheus +package io import ( "context" @@ -11,6 +11,7 @@ import ( "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/pkg/common/model" ) var ( @@ -19,29 +20,29 @@ var ( ErrNoSLORules = fmt.Errorf("0 SLO Prometheus rules generated") ) -func NewIOWriterGroupedRulesYAMLRepo(writer io.Writer, logger log.Logger) IOWriterGroupedRulesYAMLRepo { - return IOWriterGroupedRulesYAMLRepo{ +func NewGroupedRulesYAMLRepo(writer io.Writer, logger log.Logger) GroupedRulesYAMLRepo { + return GroupedRulesYAMLRepo{ writer: writer, - logger: logger.WithValues(log.Kv{"svc": "storage.IOWriter", "format": "yaml"}), + logger: logger.WithValues(log.Kv{"svc": "storageio.GroupedRulesYAMLRepo"}), } } -// IOWriterGroupedRulesYAMLRepo knows to store all the SLO rules (recordings and alerts) +// GroupedRulesYAMLRepo knows to store all the SLO rules (recordings and alerts) // grouped in an IOWriter in YAML format, that is compatible with Prometheus. -type IOWriterGroupedRulesYAMLRepo struct { +type GroupedRulesYAMLRepo struct { writer io.Writer logger log.Logger } type StorageSLO struct { - SLO SLO - Rules SLORules + SLO model.PromSLO + Rules model.PromSLORules } // StoreSLOs will store the recording and alert prometheus rules, if grouped is false it will // split and store as 2 different groups the alerts and the recordings, if true // it will be save as a single group. -func (i IOWriterGroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos []StorageSLO) error { +func (g GroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos []StorageSLO) error { if len(slos) == 0 { return fmt.Errorf("slo rules required") } @@ -83,12 +84,12 @@ func (i IOWriterGroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos []Stor } rulesYaml = writeTopDisclaimer(rulesYaml) - _, err = i.writer.Write(rulesYaml) + _, err = g.writer.Write(rulesYaml) if err != nil { return fmt.Errorf("could not write top disclaimer: %w", err) } - logger := i.logger.WithCtxValues(ctx) + logger := g.logger.WithCtxValues(ctx) logger.WithValues(log.Kv{"groups": len(ruleGroups.Groups)}).Infof("Prometheus rules written") return nil diff --git a/internal/prometheus/storage_test.go b/internal/storage/io/slo_prometheus_test.go similarity index 87% rename from internal/prometheus/storage_test.go rename to internal/storage/io/slo_prometheus_test.go index 545c692b..b4dbf0e1 100644 --- a/internal/prometheus/storage_test.go +++ b/internal/storage/io/slo_prometheus_test.go @@ -1,4 +1,4 @@ -package prometheus_test +package io_test import ( "bytes" @@ -9,32 +9,33 @@ import ( "github.com/stretchr/testify/assert" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/internal/storage/io" + "github.com/slok/sloth/pkg/common/model" ) -func TestIOWriterGroupedRulesYAMLRepoStore(t *testing.T) { +func TestGroupedRulesYAMLRepoStore(t *testing.T) { tests := map[string]struct { - slos []prometheus.StorageSLO + slos []io.StorageSLO expYAML string expErr bool }{ "Having 0 SLO rules should fail.": { - slos: []prometheus.StorageSLO{}, + slos: []io.StorageSLO{}, expErr: true, }, "Having 0 SLO rules generated should fail.": { - slos: []prometheus.StorageSLO{ + slos: []io.StorageSLO{ {}, }, expErr: true, }, "Having a single SLI recording rule should render correctly.": { - slos: []prometheus.StorageSLO{ + slos: []io.StorageSLO{ { - SLO: prometheus.SLO{ID: "test1"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "test1"}, + Rules: model.PromSLORules{ SLIErrorRecRules: []rulefmt.Rule{ { Record: "test:record", @@ -60,10 +61,10 @@ groups: `, }, "Having a single metadata recording rule should render correctly.": { - slos: []prometheus.StorageSLO{ + slos: []io.StorageSLO{ { - SLO: prometheus.SLO{ID: "test1"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "test1"}, + Rules: model.PromSLORules{ MetadataRecRules: []rulefmt.Rule{ { Record: "test:record", @@ -89,10 +90,10 @@ groups: `, }, "Having a single SLO alert rule should render correctly.": { - slos: []prometheus.StorageSLO{ + slos: []io.StorageSLO{ { - SLO: prometheus.SLO{ID: "test1"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "test1"}, + Rules: model.PromSLORules{ AlertRules: []rulefmt.Rule{ { Alert: "testAlert", @@ -122,10 +123,10 @@ groups: }, "Having a multiple SLO alert and recording rules should render correctly.": { - slos: []prometheus.StorageSLO{ + slos: []io.StorageSLO{ { - SLO: prometheus.SLO{ID: "testa"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "testa"}, + Rules: model.PromSLORules{ SLIErrorRecRules: []rulefmt.Rule{ { Record: "test:record-a1", @@ -167,8 +168,8 @@ groups: }, }, { - SLO: prometheus.SLO{ID: "testb"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "testb"}, + Rules: model.PromSLORules{ SLIErrorRecRules: []rulefmt.Rule{ { Record: "test:record-b1", @@ -263,7 +264,7 @@ groups: assert := assert.New(t) var gotYAML bytes.Buffer - repo := prometheus.NewIOWriterGroupedRulesYAMLRepo(&gotYAML, log.Noop) + repo := io.NewGroupedRulesYAMLRepo(&gotYAML, log.Noop) err := repo.StoreSLOs(context.TODO(), test.slos) if test.expErr { From 913905a6be3e87ce54bbf676038073d9f8c15b21 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 29 Mar 2025 11:22:33 +0100 Subject: [PATCH 022/173] Rename ported io storage to be explicit about the prometheus type specs Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 12 ++--- internal/storage/io/helper.go | 18 ++++++++ ...lo_prometheus.go => slo_std_prometheus.go} | 46 +++++++------------ ...eus_test.go => slo_std_prometheus_test.go} | 16 +++---- 4 files changed, 49 insertions(+), 43 deletions(-) create mode 100644 internal/storage/io/helper.go rename internal/storage/io/{slo_prometheus.go => slo_std_prometheus.go} (67%) rename internal/storage/io/{slo_prometheus_test.go => slo_std_prometheus_test.go} (94%) diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index def71e7f..6c610a88 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -323,10 +323,10 @@ func (g generator) GeneratePrometheus(ctx context.Context, slos prometheus.SLOGr return err } - repo := storageio.NewGroupedRulesYAMLRepo(out, g.logger) - storageSLOs := make([]storageio.StorageSLO, 0, len(result.PrometheusSLOs)) + repo := storageio.NewStdPrometheusGroupedRulesYAMLRepo(out, g.logger) + storageSLOs := make([]storageio.StdPrometheusStorageSLO, 0, len(result.PrometheusSLOs)) for _, s := range result.PrometheusSLOs { - storageSLOs = append(storageSLOs, storageio.StorageSLO{ + storageSLOs = append(storageSLOs, storageio.StdPrometheusStorageSLO{ SLO: s.SLO, Rules: s.SLORules, }) @@ -385,10 +385,10 @@ func (g generator) GenerateOpenSLO(ctx context.Context, slos prometheus.SLOGroup return err } - repo := storageio.NewGroupedRulesYAMLRepo(out, g.logger) - storageSLOs := make([]storageio.StorageSLO, 0, len(result.PrometheusSLOs)) + repo := storageio.NewStdPrometheusGroupedRulesYAMLRepo(out, g.logger) + storageSLOs := make([]storageio.StdPrometheusStorageSLO, 0, len(result.PrometheusSLOs)) for _, s := range result.PrometheusSLOs { - storageSLOs = append(storageSLOs, storageio.StorageSLO{ + storageSLOs = append(storageSLOs, storageio.StdPrometheusStorageSLO{ SLO: s.SLO, Rules: s.SLORules, }) diff --git a/internal/storage/io/helper.go b/internal/storage/io/helper.go new file mode 100644 index 00000000..8cca3944 --- /dev/null +++ b/internal/storage/io/helper.go @@ -0,0 +1,18 @@ +package io + +import ( + "fmt" + + "github.com/slok/sloth/internal/info" +) + +var yamlTopdisclaimer = fmt.Sprintf(` +--- +# Code generated by Sloth (%s): https://github.com/slok/sloth. +# DO NOT EDIT. + +`, info.Version) + +func writeYAMLTopDisclaimer(bs []byte) []byte { + return append([]byte(yamlTopdisclaimer), bs...) +} diff --git a/internal/storage/io/slo_prometheus.go b/internal/storage/io/slo_std_prometheus.go similarity index 67% rename from internal/storage/io/slo_prometheus.go rename to internal/storage/io/slo_std_prometheus.go index c0cd5340..393fa8df 100644 --- a/internal/storage/io/slo_prometheus.go +++ b/internal/storage/io/slo_std_prometheus.go @@ -9,7 +9,6 @@ import ( "github.com/prometheus/prometheus/model/rulefmt" "gopkg.in/yaml.v2" - "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/pkg/common/model" ) @@ -20,21 +19,21 @@ var ( ErrNoSLORules = fmt.Errorf("0 SLO Prometheus rules generated") ) -func NewGroupedRulesYAMLRepo(writer io.Writer, logger log.Logger) GroupedRulesYAMLRepo { - return GroupedRulesYAMLRepo{ +func NewStdPrometheusGroupedRulesYAMLRepo(writer io.Writer, logger log.Logger) StdPrometheusGroupedRulesYAMLRepo { + return StdPrometheusGroupedRulesYAMLRepo{ writer: writer, - logger: logger.WithValues(log.Kv{"svc": "storageio.GroupedRulesYAMLRepo"}), + logger: logger.WithValues(log.Kv{"svc": "storageio.StdPrometheusGroupedRulesYAMLRepo"}), } } -// GroupedRulesYAMLRepo knows to store all the SLO rules (recordings and alerts) +// StdPrometheusGroupedRulesYAMLRepo knows to store all the SLO rules (recordings and alerts) // grouped in an IOWriter in YAML format, that is compatible with Prometheus. -type GroupedRulesYAMLRepo struct { +type StdPrometheusGroupedRulesYAMLRepo struct { writer io.Writer logger log.Logger } -type StorageSLO struct { +type StdPrometheusStorageSLO struct { SLO model.PromSLO Rules model.PromSLORules } @@ -42,29 +41,29 @@ type StorageSLO struct { // StoreSLOs will store the recording and alert prometheus rules, if grouped is false it will // split and store as 2 different groups the alerts and the recordings, if true // it will be save as a single group. -func (g GroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos []StorageSLO) error { +func (r StdPrometheusGroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos []StdPrometheusStorageSLO) error { if len(slos) == 0 { return fmt.Errorf("slo rules required") } - ruleGroups := ruleGroupsYAMLv2{} + ruleGroups := stdPromRuleGroupsYAMLv2{} for _, slo := range slos { if len(slo.Rules.SLIErrorRecRules) > 0 { - ruleGroups.Groups = append(ruleGroups.Groups, ruleGroupYAMLv2{ + ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), Rules: slo.Rules.SLIErrorRecRules, }) } if len(slo.Rules.MetadataRecRules) > 0 { - ruleGroups.Groups = append(ruleGroups.Groups, ruleGroupYAMLv2{ + ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), Rules: slo.Rules.MetadataRecRules, }) } if len(slo.Rules.AlertRules) > 0 { - ruleGroups.Groups = append(ruleGroups.Groups, ruleGroupYAMLv2{ + ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), Rules: slo.Rules.AlertRules, }) @@ -83,36 +82,25 @@ func (g GroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos []StorageSLO) return fmt.Errorf("could not format rules: %w", err) } - rulesYaml = writeTopDisclaimer(rulesYaml) - _, err = g.writer.Write(rulesYaml) + rulesYaml = writeYAMLTopDisclaimer(rulesYaml) + _, err = r.writer.Write(rulesYaml) if err != nil { return fmt.Errorf("could not write top disclaimer: %w", err) } - logger := g.logger.WithCtxValues(ctx) + logger := r.logger.WithCtxValues(ctx) logger.WithValues(log.Kv{"groups": len(ruleGroups.Groups)}).Infof("Prometheus rules written") return nil } -var disclaimer = fmt.Sprintf(` ---- -# Code generated by Sloth (%s): https://github.com/slok/sloth. -# DO NOT EDIT. - -`, info.Version) - -func writeTopDisclaimer(bs []byte) []byte { - return append([]byte(disclaimer), bs...) -} - // these types are defined to support yaml v2 (instead of the new Prometheus // YAML v3 that has some problems with marshaling). -type ruleGroupsYAMLv2 struct { - Groups []ruleGroupYAMLv2 `yaml:"groups"` +type stdPromRuleGroupsYAMLv2 struct { + Groups []stdPromRuleGroupYAMLv2 `yaml:"groups"` } -type ruleGroupYAMLv2 struct { +type stdPromRuleGroupYAMLv2 struct { Name string `yaml:"name"` Interval prommodel.Duration `yaml:"interval,omitempty"` Rules []rulefmt.Rule `yaml:"rules"` diff --git a/internal/storage/io/slo_prometheus_test.go b/internal/storage/io/slo_std_prometheus_test.go similarity index 94% rename from internal/storage/io/slo_prometheus_test.go rename to internal/storage/io/slo_std_prometheus_test.go index b4dbf0e1..24ee9dae 100644 --- a/internal/storage/io/slo_prometheus_test.go +++ b/internal/storage/io/slo_std_prometheus_test.go @@ -15,24 +15,24 @@ import ( func TestGroupedRulesYAMLRepoStore(t *testing.T) { tests := map[string]struct { - slos []io.StorageSLO + slos []io.StdPrometheusStorageSLO expYAML string expErr bool }{ "Having 0 SLO rules should fail.": { - slos: []io.StorageSLO{}, + slos: []io.StdPrometheusStorageSLO{}, expErr: true, }, "Having 0 SLO rules generated should fail.": { - slos: []io.StorageSLO{ + slos: []io.StdPrometheusStorageSLO{ {}, }, expErr: true, }, "Having a single SLI recording rule should render correctly.": { - slos: []io.StorageSLO{ + slos: []io.StdPrometheusStorageSLO{ { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ @@ -61,7 +61,7 @@ groups: `, }, "Having a single metadata recording rule should render correctly.": { - slos: []io.StorageSLO{ + slos: []io.StdPrometheusStorageSLO{ { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ @@ -90,7 +90,7 @@ groups: `, }, "Having a single SLO alert rule should render correctly.": { - slos: []io.StorageSLO{ + slos: []io.StdPrometheusStorageSLO{ { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ @@ -123,7 +123,7 @@ groups: }, "Having a multiple SLO alert and recording rules should render correctly.": { - slos: []io.StorageSLO{ + slos: []io.StdPrometheusStorageSLO{ { SLO: model.PromSLO{ID: "testa"}, Rules: model.PromSLORules{ @@ -264,7 +264,7 @@ groups: assert := assert.New(t) var gotYAML bytes.Buffer - repo := io.NewGroupedRulesYAMLRepo(&gotYAML, log.Noop) + repo := io.NewStdPrometheusGroupedRulesYAMLRepo(&gotYAML, log.Noop) err := repo.StoreSLOs(context.TODO(), test.slos) if test.expErr { From 3826e2a33bad329bbde9598309a2081b18021d58 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 29 Mar 2025 16:56:00 +0100 Subject: [PATCH 023/173] Move merge label to utils Signed-off-by: Xabier Larrakoetxea --- internal/app/generate/prometheus.go | 14 ++----------- internal/k8sprometheus/helpers.go | 12 ----------- internal/k8sprometheus/spec.go | 11 +++++----- internal/prometheus/alert_rules.go | 5 +++-- internal/prometheus/recording_rules.go | 20 ++++++------------- internal/prometheus/spec.go | 11 +++++----- ...lo_std_prometheus.go => std_prometheus.go} | 0 ...metheus_test.go => std_prometheus_test.go} | 0 pkg/common/utils/data/data.go | 11 ++++++++++ 9 files changed, 34 insertions(+), 50 deletions(-) delete mode 100644 internal/k8sprometheus/helpers.go rename internal/storage/io/{slo_std_prometheus.go => std_prometheus.go} (100%) rename internal/storage/io/{slo_std_prometheus_test.go => std_prometheus_test.go} (100%) create mode 100644 pkg/common/utils/data/data.go diff --git a/internal/app/generate/prometheus.go b/internal/app/generate/prometheus.go index bb677f44..fd0e2976 100644 --- a/internal/app/generate/prometheus.go +++ b/internal/app/generate/prometheus.go @@ -10,6 +10,7 @@ import ( "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/prometheus" "github.com/slok/sloth/pkg/common/model" + utilsdata "github.com/slok/sloth/pkg/common/utils/data" ) // ServiceConfig is the application service configuration. @@ -120,7 +121,7 @@ func (s Service) Generate(ctx context.Context, r Request) (*Response, error) { results := make([]SLOResult, 0, len(r.SLOGroup.SLOs)) for _, slo := range r.SLOGroup.SLOs { // Add extra labels. - slo.Labels = mergeLabels(slo.Labels, r.ExtraLabels) + slo.Labels = utilsdata.MergeLabels(slo.Labels, r.ExtraLabels) // Generate SLO result. result, err := s.generateSLO(ctx, r.Info, slo) @@ -182,14 +183,3 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, slo prometheu }, }, nil } - -func mergeLabels(ms ...map[string]string) map[string]string { - res := map[string]string{} - for _, m := range ms { - for k, v := range m { - res[k] = v - } - } - - return res -} diff --git a/internal/k8sprometheus/helpers.go b/internal/k8sprometheus/helpers.go deleted file mode 100644 index 541e202b..00000000 --- a/internal/k8sprometheus/helpers.go +++ /dev/null @@ -1,12 +0,0 @@ -package k8sprometheus - -func mergeLabels(ms ...map[string]string) map[string]string { - res := map[string]string{} - for _, m := range ms { - for k, v := range m { - res[k] = v - } - } - - return res -} diff --git a/internal/k8sprometheus/spec.go b/internal/k8sprometheus/spec.go index 2f8bdec7..ba0e62a3 100644 --- a/internal/k8sprometheus/spec.go +++ b/internal/k8sprometheus/spec.go @@ -10,6 +10,7 @@ import ( pluginsli "github.com/slok/sloth/internal/plugin/sli" "github.com/slok/sloth/internal/prometheus" + utilsdata "github.com/slok/sloth/pkg/common/utils/data" k8sprometheusv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/scheme" prometheuspluginv1 "github.com/slok/sloth/pkg/prometheus/plugin/v1" @@ -101,7 +102,7 @@ func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, plug Service: spec.Service, TimeWindow: defaultWindowPeriod, Objective: specSLO.Objective, - Labels: mergeLabels(spec.Labels, specSLO.Labels), + Labels: utilsdata.MergeLabels(spec.Labels, specSLO.Labels), PageAlertMeta: prometheus.AlertMeta{Disable: true}, TicketAlertMeta: prometheus.AlertMeta{Disable: true}, } @@ -146,16 +147,16 @@ func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, plug if !specSLO.Alerting.PageAlert.Disable { slo.PageAlertMeta = prometheus.AlertMeta{ Name: specSLO.Alerting.Name, - Labels: mergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.PageAlert.Labels), - Annotations: mergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.PageAlert.Annotations), + Labels: utilsdata.MergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.PageAlert.Labels), + Annotations: utilsdata.MergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.PageAlert.Annotations), } } if !specSLO.Alerting.TicketAlert.Disable { slo.TicketAlertMeta = prometheus.AlertMeta{ Name: specSLO.Alerting.Name, - Labels: mergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.TicketAlert.Labels), - Annotations: mergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.TicketAlert.Annotations), + Labels: utilsdata.MergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.TicketAlert.Labels), + Annotations: utilsdata.MergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.TicketAlert.Annotations), } } diff --git a/internal/prometheus/alert_rules.go b/internal/prometheus/alert_rules.go index 73281033..c9b2777b 100644 --- a/internal/prometheus/alert_rules.go +++ b/internal/prometheus/alert_rules.go @@ -10,6 +10,7 @@ import ( "github.com/slok/sloth/pkg/common/conventions" "github.com/slok/sloth/pkg/common/model" + utilsdata "github.com/slok/sloth/pkg/common/utils/data" promutils "github.com/slok/sloth/pkg/common/utils/prometheus" ) @@ -102,8 +103,8 @@ func defaultSLOAlertGenerator(slo SLO, sloAlert AlertMeta, quick, slow model.MWM return &rulefmt.Rule{ Alert: sloAlert.Name, Expr: expr.String(), - Annotations: mergeLabels(extraAnnotations, sloAlert.Annotations), - Labels: mergeLabels(extraLabels, sloAlert.Labels), + Annotations: utilsdata.MergeLabels(extraAnnotations, sloAlert.Annotations), + Labels: utilsdata.MergeLabels(extraLabels, sloAlert.Labels), }, nil } diff --git a/internal/prometheus/recording_rules.go b/internal/prometheus/recording_rules.go index dcf92fd4..515c3b12 100644 --- a/internal/prometheus/recording_rules.go +++ b/internal/prometheus/recording_rules.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "maps" "sort" "strconv" "text/template" @@ -14,6 +13,7 @@ import ( "github.com/slok/sloth/pkg/common/conventions" "github.com/slok/sloth/pkg/common/model" + utilsdata "github.com/slok/sloth/pkg/common/utils/data" promutils "github.com/slok/sloth/pkg/common/utils/prometheus" ) @@ -98,7 +98,7 @@ func rawSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlert return &rulefmt.Rule{ Record: conventions.GetSLIErrorMetric(window), Expr: b.String(), - Labels: mergeLabels( + Labels: utilsdata.MergeLabels( conventions.GetSLOIDPromLabels(slo), map[string]string{ conventions.PromSLOWindowLabelName: strWindow, @@ -134,7 +134,7 @@ func eventsSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAl return &rulefmt.Rule{ Record: conventions.GetSLIErrorMetric(window), Expr: b.String(), - Labels: mergeLabels( + Labels: utilsdata.MergeLabels( conventions.GetSLOIDPromLabels(slo), map[string]string{ conventions.PromSLOWindowLabelName: strWindow, @@ -189,7 +189,7 @@ count_over_time({{.metric}}{{.filter}}[{{.window}}]) return &rulefmt.Rule{ Record: conventions.GetSLIErrorMetric(window), Expr: b.String(), - Labels: mergeLabels( + Labels: utilsdata.MergeLabels( conventions.GetSLOIDPromLabels(slo), map[string]string{ conventions.PromSLOWindowLabelName: strWindow, @@ -206,7 +206,7 @@ type metadataRecordingRulesGenerator bool const MetadataRecordingRulesGenerator = metadataRecordingRulesGenerator(false) func (m metadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx context.Context, info model.Info, slo SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { - labels := mergeLabels(conventions.GetSLOIDPromLabels(slo), slo.Labels) + labels := utilsdata.MergeLabels(conventions.GetSLOIDPromLabels(slo), slo.Labels) // Metatada Recordings. const ( @@ -296,7 +296,7 @@ func (m metadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx cont { Record: metricSLOInfo, Expr: `vector(1)`, - Labels: mergeLabels(labels, map[string]string{ + Labels: utilsdata.MergeLabels(labels, map[string]string{ conventions.PromSLOVersionLabelName: info.Version, conventions.PromSLOModeLabelName: string(info.Mode), conventions.PromSLOSpecLabelName: info.Spec, @@ -335,11 +335,3 @@ func getAlertGroupWindows(alerts model.MWMBAlertGroup) []time.Duration { return res } - -func mergeLabels[M ~map[K]V, K comparable, V any](ms ...M) M { - m := make(M) - for _, m2 := range ms { - maps.Copy(m, m2) - } - return m -} diff --git a/internal/prometheus/spec.go b/internal/prometheus/spec.go index 2fdab87a..01dce8aa 100644 --- a/internal/prometheus/spec.go +++ b/internal/prometheus/spec.go @@ -9,6 +9,7 @@ import ( "gopkg.in/yaml.v2" pluginsli "github.com/slok/sloth/internal/plugin/sli" + utilsdata "github.com/slok/sloth/pkg/common/utils/data" prometheusv1 "github.com/slok/sloth/pkg/prometheus/api/v1" prometheuspluginv1 "github.com/slok/sloth/pkg/prometheus/plugin/v1" ) @@ -76,7 +77,7 @@ func (y YAMLSpecLoader) mapSpecToModel(ctx context.Context, spec prometheusv1.Sp Service: spec.Service, TimeWindow: y.windowPeriod, Objective: specSLO.Objective, - Labels: mergeLabels(spec.Labels, specSLO.Labels), + Labels: utilsdata.MergeLabels(spec.Labels, specSLO.Labels), PageAlertMeta: AlertMeta{Disable: true}, TicketAlertMeta: AlertMeta{Disable: true}, } @@ -121,16 +122,16 @@ func (y YAMLSpecLoader) mapSpecToModel(ctx context.Context, spec prometheusv1.Sp if !specSLO.Alerting.PageAlert.Disable { slo.PageAlertMeta = AlertMeta{ Name: specSLO.Alerting.Name, - Labels: mergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.PageAlert.Labels), - Annotations: mergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.PageAlert.Annotations), + Labels: utilsdata.MergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.PageAlert.Labels), + Annotations: utilsdata.MergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.PageAlert.Annotations), } } if !specSLO.Alerting.TicketAlert.Disable { slo.TicketAlertMeta = AlertMeta{ Name: specSLO.Alerting.Name, - Labels: mergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.TicketAlert.Labels), - Annotations: mergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.TicketAlert.Annotations), + Labels: utilsdata.MergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.TicketAlert.Labels), + Annotations: utilsdata.MergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.TicketAlert.Annotations), } } diff --git a/internal/storage/io/slo_std_prometheus.go b/internal/storage/io/std_prometheus.go similarity index 100% rename from internal/storage/io/slo_std_prometheus.go rename to internal/storage/io/std_prometheus.go diff --git a/internal/storage/io/slo_std_prometheus_test.go b/internal/storage/io/std_prometheus_test.go similarity index 100% rename from internal/storage/io/slo_std_prometheus_test.go rename to internal/storage/io/std_prometheus_test.go diff --git a/pkg/common/utils/data/data.go b/pkg/common/utils/data/data.go new file mode 100644 index 00000000..85ccaa6c --- /dev/null +++ b/pkg/common/utils/data/data.go @@ -0,0 +1,11 @@ +package data + +import "maps" + +func MergeLabels[M ~map[K]V, K comparable, V any](ms ...M) M { + m := make(M) + for _, m2 := range ms { + maps.Copy(m, m2) + } + return m +} From 32b2ccbf8cc7209127960e32e3cc6fe8b631b855 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 29 Mar 2025 17:32:40 +0100 Subject: [PATCH 024/173] Move sloth prom spec loader to storage package Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 2 +- cmd/sloth/commands/validate.go | 4 +- .../spec.go => storage/io/sloth.go} | 45 ++++++++++--------- .../spec_test.go => storage/io/sloth_test.go} | 7 +-- 4 files changed, 30 insertions(+), 28 deletions(-) rename internal/{prometheus/spec.go => storage/io/sloth.go} (69%) rename internal/{prometheus/spec_test.go => storage/io/sloth_test.go} (97%) diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 6c610a88..8a6b59ec 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -133,7 +133,7 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { } // Create Spec loaders. - promYAMLLoader := prometheus.NewYAMLSpecLoader(pluginRepo, sloPeriod) + promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) kubeYAMLLoader := k8sprometheus.NewYAMLSpecLoader(pluginRepo, sloPeriod) openSLOYAMLLoader := openslo.NewYAMLSpecLoader(sloPeriod) diff --git a/cmd/sloth/commands/validate.go b/cmd/sloth/commands/validate.go index efc5f8e0..1b960127 100644 --- a/cmd/sloth/commands/validate.go +++ b/cmd/sloth/commands/validate.go @@ -16,7 +16,7 @@ import ( "github.com/slok/sloth/internal/k8sprometheus" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/openslo" - "github.com/slok/sloth/internal/prometheus" + storageio "github.com/slok/sloth/internal/storage/io" ) type validateCommand struct { @@ -108,7 +108,7 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { } // Create Spec loaders. - promYAMLLoader := prometheus.NewYAMLSpecLoader(pluginRepo, sloPeriod) + promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) kubeYAMLLoader := k8sprometheus.NewYAMLSpecLoader(pluginRepo, sloPeriod) openSLOYAMLLoader := openslo.NewYAMLSpecLoader(sloPeriod) diff --git a/internal/prometheus/spec.go b/internal/storage/io/sloth.go similarity index 69% rename from internal/prometheus/spec.go rename to internal/storage/io/sloth.go index 01dce8aa..e7151bb9 100644 --- a/internal/prometheus/spec.go +++ b/internal/storage/io/sloth.go @@ -1,4 +1,4 @@ -package prometheus +package io import ( "context" @@ -9,6 +9,7 @@ import ( "gopkg.in/yaml.v2" pluginsli "github.com/slok/sloth/internal/plugin/sli" + "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" prometheusv1 "github.com/slok/sloth/pkg/prometheus/api/v1" prometheuspluginv1 "github.com/slok/sloth/pkg/prometheus/plugin/v1" @@ -18,15 +19,15 @@ type SLIPluginRepo interface { GetSLIPlugin(ctx context.Context, id string) (*pluginsli.SLIPlugin, error) } -// YAMLSpecLoader knows how to load YAML specs and converts them to a model. -type YAMLSpecLoader struct { +// SlothPrometheusYAMLSpecLoader knows how to load sloth prometheus YAML specs and converts them to a model. +type SlothPrometheusYAMLSpecLoader struct { windowPeriod time.Duration pluginsRepo SLIPluginRepo } -// NewYAMLSpecLoader returns a YAML spec loader. -func NewYAMLSpecLoader(pluginsRepo SLIPluginRepo, windowPeriod time.Duration) YAMLSpecLoader { - return YAMLSpecLoader{ +// NewSlothPrometheusYAMLSpecLoader returns a YAML spec loader. +func NewSlothPrometheusYAMLSpecLoader(pluginsRepo SLIPluginRepo, windowPeriod time.Duration) SlothPrometheusYAMLSpecLoader { + return SlothPrometheusYAMLSpecLoader{ windowPeriod: windowPeriod, pluginsRepo: pluginsRepo, } @@ -34,11 +35,11 @@ func NewYAMLSpecLoader(pluginsRepo SLIPluginRepo, windowPeriod time.Duration) YA var specTypeV1Regex = regexp.MustCompile(`(?m)^version: +['"]?prometheus/v1['"]?\r?\n? *$`) -func (y YAMLSpecLoader) IsSpecType(ctx context.Context, data []byte) bool { +func (l SlothPrometheusYAMLSpecLoader) IsSpecType(ctx context.Context, data []byte) bool { return specTypeV1Regex.Match(data) } -func (y YAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*SLOGroup, error) { +func (l SlothPrometheusYAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*model.PromSLOGroup, error) { if len(data) == 0 { return nil, fmt.Errorf("spec is required") } @@ -59,7 +60,7 @@ func (y YAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*SLOGroup, e return nil, fmt.Errorf("at least one SLO is required") } - m, err := y.mapSpecToModel(ctx, s) + m, err := l.mapSpecToModel(ctx, s) if err != nil { return nil, fmt.Errorf("could not map to model: %w", err) } @@ -67,37 +68,37 @@ func (y YAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*SLOGroup, e return m, nil } -func (y YAMLSpecLoader) mapSpecToModel(ctx context.Context, spec prometheusv1.Spec) (*SLOGroup, error) { - models := make([]SLO, 0, len(spec.SLOs)) +func (l SlothPrometheusYAMLSpecLoader) mapSpecToModel(ctx context.Context, spec prometheusv1.Spec) (*model.PromSLOGroup, error) { + models := make([]model.PromSLO, 0, len(spec.SLOs)) for _, specSLO := range spec.SLOs { - slo := SLO{ + slo := model.PromSLO{ ID: fmt.Sprintf("%s-%s", spec.Service, specSLO.Name), Name: specSLO.Name, Description: specSLO.Description, Service: spec.Service, - TimeWindow: y.windowPeriod, + TimeWindow: l.windowPeriod, Objective: specSLO.Objective, Labels: utilsdata.MergeLabels(spec.Labels, specSLO.Labels), - PageAlertMeta: AlertMeta{Disable: true}, - TicketAlertMeta: AlertMeta{Disable: true}, + PageAlertMeta: model.PromAlertMeta{Disable: true}, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, } // Set SLIs. if specSLO.SLI.Events != nil { - slo.SLI.Events = &SLIEvents{ + slo.SLI.Events = &model.PromSLIEvents{ ErrorQuery: specSLO.SLI.Events.ErrorQuery, TotalQuery: specSLO.SLI.Events.TotalQuery, } } if specSLO.SLI.Raw != nil { - slo.SLI.Raw = &SLIRaw{ + slo.SLI.Raw = &model.PromSLIRaw{ ErrorRatioQuery: specSLO.SLI.Raw.ErrorRatioQuery, } } if specSLO.SLI.Plugin != nil { - plugin, err := y.pluginsRepo.GetSLIPlugin(ctx, specSLO.SLI.Plugin.ID) + plugin, err := l.pluginsRepo.GetSLIPlugin(ctx, specSLO.SLI.Plugin.ID) if err != nil { return nil, fmt.Errorf("could not get plugin: %w", err) } @@ -113,14 +114,14 @@ func (y YAMLSpecLoader) mapSpecToModel(ctx context.Context, spec prometheusv1.Sp return nil, fmt.Errorf("plugin %q execution error: %w", specSLO.SLI.Plugin.ID, err) } - slo.SLI.Raw = &SLIRaw{ + slo.SLI.Raw = &model.PromSLIRaw{ ErrorRatioQuery: rawQuery, } } // Set alerts. if !specSLO.Alerting.PageAlert.Disable { - slo.PageAlertMeta = AlertMeta{ + slo.PageAlertMeta = model.PromAlertMeta{ Name: specSLO.Alerting.Name, Labels: utilsdata.MergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.PageAlert.Labels), Annotations: utilsdata.MergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.PageAlert.Annotations), @@ -128,7 +129,7 @@ func (y YAMLSpecLoader) mapSpecToModel(ctx context.Context, spec prometheusv1.Sp } if !specSLO.Alerting.TicketAlert.Disable { - slo.TicketAlertMeta = AlertMeta{ + slo.TicketAlertMeta = model.PromAlertMeta{ Name: specSLO.Alerting.Name, Labels: utilsdata.MergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.TicketAlert.Labels), Annotations: utilsdata.MergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.TicketAlert.Annotations), @@ -138,5 +139,5 @@ func (y YAMLSpecLoader) mapSpecToModel(ctx context.Context, spec prometheusv1.Sp models = append(models, slo) } - return &SLOGroup{SLOs: models}, nil + return &model.PromSLOGroup{SLOs: models}, nil } diff --git a/internal/prometheus/spec_test.go b/internal/storage/io/sloth_test.go similarity index 97% rename from internal/prometheus/spec_test.go rename to internal/storage/io/sloth_test.go index 4a1d46b0..ba7d2a4e 100644 --- a/internal/prometheus/spec_test.go +++ b/internal/storage/io/sloth_test.go @@ -1,4 +1,4 @@ -package prometheus_test +package io_test import ( "context" @@ -10,6 +10,7 @@ import ( pluginsli "github.com/slok/sloth/internal/plugin/sli" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/internal/storage/io" ) type testMemPluginsRepo map[string]pluginsli.SLIPlugin @@ -326,7 +327,7 @@ slos: t.Run(name, func(t *testing.T) { assert := assert.New(t) - loader := prometheus.NewYAMLSpecLoader(testMemPluginsRepo(test.plugins), test.windowPeriod) + loader := io.NewSlothPrometheusYAMLSpecLoader(testMemPluginsRepo(test.plugins), test.windowPeriod) gotModel, err := loader.LoadSpec(context.TODO(), []byte(test.specYaml)) if test.expErr { @@ -383,7 +384,7 @@ func TestYAMLIsSpecType(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - loader := prometheus.NewYAMLSpecLoader(testMemPluginsRepo(map[string]pluginsli.SLIPlugin{}), 0) + loader := io.NewSlothPrometheusYAMLSpecLoader(testMemPluginsRepo(map[string]pluginsli.SLIPlugin{}), 0) got := loader.IsSpecType(context.TODO(), []byte(test.specYaml)) assert.Equal(test.exp, got) From fa9ed4ea68495300989b0e48f4075ec83bdce965 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 29 Mar 2025 17:45:42 +0100 Subject: [PATCH 025/173] Move open SLO spec loader to storage package Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 3 +- cmd/sloth/commands/validate.go | 3 +- .../spec.go => storage/io/openslo.go} | 42 +++++++++---------- .../io/openslo_test.go} | 12 +++--- internal/storage/io/sloth.go | 4 +- internal/storage/io/sloth_test.go | 4 +- 6 files changed, 33 insertions(+), 35 deletions(-) rename internal/{openslo/spec.go => storage/io/openslo.go} (73%) rename internal/{openslo/spec_test.go => storage/io/openslo_test.go} (96%) diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 8a6b59ec..4568a8dd 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -21,7 +21,6 @@ import ( "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/k8sprometheus" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/openslo" "github.com/slok/sloth/internal/prometheus" storageio "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" @@ -135,7 +134,7 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { // Create Spec loaders. promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) kubeYAMLLoader := k8sprometheus.NewYAMLSpecLoader(pluginRepo, sloPeriod) - openSLOYAMLLoader := openslo.NewYAMLSpecLoader(sloPeriod) + openSLOYAMLLoader := storageio.NewOpenSLOYAMLSpecLoader(sloPeriod) // Get SLO targets. genTargets := []generateTarget{} diff --git a/cmd/sloth/commands/validate.go b/cmd/sloth/commands/validate.go index 1b960127..37e95f87 100644 --- a/cmd/sloth/commands/validate.go +++ b/cmd/sloth/commands/validate.go @@ -15,7 +15,6 @@ import ( "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/k8sprometheus" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/openslo" storageio "github.com/slok/sloth/internal/storage/io" ) @@ -110,7 +109,7 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { // Create Spec loaders. promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) kubeYAMLLoader := k8sprometheus.NewYAMLSpecLoader(pluginRepo, sloPeriod) - openSLOYAMLLoader := openslo.NewYAMLSpecLoader(sloPeriod) + openSLOYAMLLoader := storageio.NewOpenSLOYAMLSpecLoader(sloPeriod) // For every file load the data and start the validation process: validations := []*fileValidation{} diff --git a/internal/openslo/spec.go b/internal/storage/io/openslo.go similarity index 73% rename from internal/openslo/spec.go rename to internal/storage/io/openslo.go index d659cd21..7e5c3762 100644 --- a/internal/openslo/spec.go +++ b/internal/storage/io/openslo.go @@ -1,4 +1,4 @@ -package openslo +package io import ( "bytes" @@ -15,27 +15,27 @@ import ( "github.com/slok/sloth/internal/prometheus" ) -type YAMLSpecLoader struct { +type OpenSLOYAMLSpecLoader struct { windowPeriod time.Duration } -// YAMLSpecLoader knows how to load YAML specs and converts them to a model. -func NewYAMLSpecLoader(windowPeriod time.Duration) YAMLSpecLoader { - return YAMLSpecLoader{ +// NewOpenSLOYAMLSpecLoader knows how to load OpenSLO YAML specs and converts them to a model. +func NewOpenSLOYAMLSpecLoader(windowPeriod time.Duration) OpenSLOYAMLSpecLoader { + return OpenSLOYAMLSpecLoader{ windowPeriod: windowPeriod, } } var ( - specTypeV1AlphaRegexKind = regexp.MustCompile(`(?m)^kind: +['"]?SLO['"]? *$`) - specTypeV1AlphaRegexAPIVersion = regexp.MustCompile(`(?m)^apiVersion: +['"]?openslo\/v1alpha['"]? *$`) + openSLOSpecV1AlphaTypeRegexKind = regexp.MustCompile(`(?m)^kind: +['"]?SLO['"]? *$`) + openSLOSpecV1AlphaTypeRegexAPIVersion = regexp.MustCompile(`(?m)^apiVersion: +['"]?openslo\/v1alpha['"]? *$`) ) -func (y YAMLSpecLoader) IsSpecType(ctx context.Context, data []byte) bool { - return specTypeV1AlphaRegexKind.Match(data) && specTypeV1AlphaRegexAPIVersion.Match(data) +func (l OpenSLOYAMLSpecLoader) IsSpecType(ctx context.Context, data []byte) bool { + return openSLOSpecV1AlphaTypeRegexKind.Match(data) && openSLOSpecV1AlphaTypeRegexAPIVersion.Match(data) } -func (y YAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*prometheus.SLOGroup, error) { +func (l OpenSLOYAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*prometheus.SLOGroup, error) { if len(data) == 0 { return nil, fmt.Errorf("spec is required") } @@ -57,12 +57,12 @@ func (y YAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*prometheus. } // Validate time windows are correct. - err = y.validateTimeWindow(s) + err = l.validateTimeWindow(s) if err != nil { return nil, fmt.Errorf("invalid SLO time windows: %w", err) } - m, err := y.mapSpecToModel(s) + m, err := l.mapSpecToModel(s) if err != nil { return nil, fmt.Errorf("could not map to model: %w", err) } @@ -70,8 +70,8 @@ func (y YAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*prometheus. return m, nil } -func (y YAMLSpecLoader) mapSpecToModel(spec openslov1alpha.SLO) (*prometheus.SLOGroup, error) { - slos, err := y.getSLOs(spec) +func (l OpenSLOYAMLSpecLoader) mapSpecToModel(spec openslov1alpha.SLO) (*prometheus.SLOGroup, error) { + slos, err := l.getSLOs(spec) if err != nil { return nil, fmt.Errorf("could not map SLOs correctly: %w", err) } @@ -81,7 +81,7 @@ func (y YAMLSpecLoader) mapSpecToModel(spec openslov1alpha.SLO) (*prometheus.SLO // validateTimeWindow will validate that Sloth only supports 30 day based time windows // we need this because time windows are a required by OpenSLO. -func (YAMLSpecLoader) validateTimeWindow(spec openslov1alpha.SLO) error { +func (OpenSLOYAMLSpecLoader) validateTimeWindow(spec openslov1alpha.SLO) error { if len(spec.Spec.TimeWindows) == 0 { return nil } @@ -98,7 +98,7 @@ func (YAMLSpecLoader) validateTimeWindow(spec openslov1alpha.SLO) error { return nil } -var errorRatioRawQueryTpl = template.Must(template.New("").Parse(` +var openSLOErrorRatioRawQueryTpl = template.Must(template.New("").Parse(` 1 - ( ( {{ .good }} @@ -114,7 +114,7 @@ var errorRatioRawQueryTpl = template.Must(template.New("").Parse(` // however we will convert to a raw based sloth SLI because the ratio queries that we have differ from // Sloth. Sloth uses bad/total events, OpenSLO uses good/total events. We get the ratio using good events // and then rest to 1, to get a raw error ratio query. -func (y YAMLSpecLoader) getSLI(spec openslov1alpha.SLOSpec, slo openslov1alpha.Objective) (*prometheus.SLI, error) { +func (l OpenSLOYAMLSpecLoader) getSLI(spec openslov1alpha.SLOSpec, slo openslov1alpha.Objective) (*prometheus.SLI, error) { if slo.RatioMetrics == nil { return nil, fmt.Errorf("missing ratioMetrics") } @@ -140,7 +140,7 @@ func (y YAMLSpecLoader) getSLI(spec openslov1alpha.SLOSpec, slo openslov1alpha.O // Map as good and total events as a raw query. var b bytes.Buffer - err := errorRatioRawQueryTpl.Execute(&b, map[string]string{"good": good.Query, "total": total.Query}) + err := openSLOErrorRatioRawQueryTpl.Execute(&b, map[string]string{"good": good.Query, "total": total.Query}) if err != nil { return nil, fmt.Errorf("could not execute mapping SLI template: %w", err) } @@ -153,16 +153,16 @@ func (y YAMLSpecLoader) getSLI(spec openslov1alpha.SLOSpec, slo openslov1alpha.O // getSLOs will try getting all the objectives as individual SLOs, this way we can map // to what Sloth understands as an SLO, that OpenSLO understands as a list of objectives // for the same SLO. -func (y YAMLSpecLoader) getSLOs(spec openslov1alpha.SLO) ([]prometheus.SLO, error) { +func (l OpenSLOYAMLSpecLoader) getSLOs(spec openslov1alpha.SLO) ([]prometheus.SLO, error) { res := []prometheus.SLO{} for idx, slo := range spec.Spec.Objectives { - sli, err := y.getSLI(spec.Spec, slo) + sli, err := l.getSLI(spec.Spec, slo) if err != nil { return nil, fmt.Errorf("could not map SLI: %w", err) } - timeWindow := y.windowPeriod + timeWindow := l.windowPeriod if len(spec.Spec.TimeWindows) > 0 { timeWindow = time.Duration(spec.Spec.TimeWindows[0].Count) * 24 * time.Hour } diff --git a/internal/openslo/spec_test.go b/internal/storage/io/openslo_test.go similarity index 96% rename from internal/openslo/spec_test.go rename to internal/storage/io/openslo_test.go index cf0dd5e2..dac88c3b 100644 --- a/internal/openslo/spec_test.go +++ b/internal/storage/io/openslo_test.go @@ -1,4 +1,4 @@ -package openslo_test +package io_test import ( "context" @@ -7,11 +7,11 @@ import ( "github.com/stretchr/testify/assert" - "github.com/slok/sloth/internal/openslo" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/internal/storage/io" ) -func TestYAMLoadSpec(t *testing.T) { +func TestOpenSLOYAMLSpecLoader(t *testing.T) { tests := map[string]struct { specYaml string expModel *prometheus.SLOGroup @@ -277,7 +277,7 @@ spec: t.Run(name, func(t *testing.T) { assert := assert.New(t) - loader := openslo.NewYAMLSpecLoader(30 * 24 * time.Hour) + loader := io.NewOpenSLOYAMLSpecLoader(30 * 24 * time.Hour) gotModel, err := loader.LoadSpec(context.TODO(), []byte(test.specYaml)) if test.expErr { @@ -289,7 +289,7 @@ spec: } } -func TestYAMLIsSpecType(t *testing.T) { +func TestOpenSLOYAMLSpecLoaderIsSpecType(t *testing.T) { tests := map[string]struct { specYaml string exp bool @@ -357,7 +357,7 @@ kind: SLO t.Run(name, func(t *testing.T) { assert := assert.New(t) - loader := openslo.NewYAMLSpecLoader(30 * 24 * time.Hour) + loader := io.NewOpenSLOYAMLSpecLoader(30 * 24 * time.Hour) got := loader.IsSpecType(context.TODO(), []byte(test.specYaml)) assert.Equal(test.exp, got) diff --git a/internal/storage/io/sloth.go b/internal/storage/io/sloth.go index e7151bb9..e3ca96a7 100644 --- a/internal/storage/io/sloth.go +++ b/internal/storage/io/sloth.go @@ -33,10 +33,10 @@ func NewSlothPrometheusYAMLSpecLoader(pluginsRepo SLIPluginRepo, windowPeriod ti } } -var specTypeV1Regex = regexp.MustCompile(`(?m)^version: +['"]?prometheus/v1['"]?\r?\n? *$`) +var slothPromSpecTypeV1Regex = regexp.MustCompile(`(?m)^version: +['"]?prometheus/v1['"]?\r?\n? *$`) func (l SlothPrometheusYAMLSpecLoader) IsSpecType(ctx context.Context, data []byte) bool { - return specTypeV1Regex.Match(data) + return slothPromSpecTypeV1Regex.Match(data) } func (l SlothPrometheusYAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*model.PromSLOGroup, error) { diff --git a/internal/storage/io/sloth_test.go b/internal/storage/io/sloth_test.go index ba7d2a4e..0a08ead6 100644 --- a/internal/storage/io/sloth_test.go +++ b/internal/storage/io/sloth_test.go @@ -23,7 +23,7 @@ func (t testMemPluginsRepo) GetSLIPlugin(ctx context.Context, id string) (*plugi return &p, nil } -func TestYAMLoadSpec(t *testing.T) { +func TestSlothPrometheusYAMLSpecLoader(t *testing.T) { tests := map[string]struct { specYaml string plugins map[string]pluginsli.SLIPlugin @@ -339,7 +339,7 @@ slos: } } -func TestYAMLIsSpecType(t *testing.T) { +func TestSlothPrometheusYAMLSpecLoaderIsSpecType(t *testing.T) { tests := map[string]struct { specYaml string exp bool From e987af73c63d3c401d29f7372e2f872823a2e14a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 07:30:54 +0000 Subject: [PATCH 026/173] build(deps): bump github.com/go-playground/validator/v10 Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.25.0 to 10.26.0. - [Release notes](https://github.com/go-playground/validator/releases) - [Commits](https://github.com/go-playground/validator/compare/v10.25.0...v10.26.0) --- updated-dependencies: - dependency-name: github.com/go-playground/validator/v10 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d67e5826..f08f870d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24 require ( github.com/OpenSLO/oslo v0.12.0 github.com/alecthomas/kingpin/v2 v2.4.0 - github.com/go-playground/validator/v10 v10.25.0 + github.com/go-playground/validator/v10 v10.26.0 github.com/oklog/run v1.1.0 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.81.0 github.com/prometheus-operator/prometheus-operator/pkg/client v0.81.0 diff --git a/go.sum b/go.sum index 3b4b954a..6dc15f8d 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= -github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= From 7165ba2bdaa03bef7396c46e082a964ffd4aa594 Mon Sep 17 00:00:00 2001 From: QuantumEnigmaa Date: Mon, 31 Mar 2025 14:36:43 +0200 Subject: [PATCH 027/173] update helm chart test Signed-off-by: QuantumEnigmaa --- deploy/kubernetes/helm/sloth/tests/values_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/deploy/kubernetes/helm/sloth/tests/values_test.go b/deploy/kubernetes/helm/sloth/tests/values_test.go index bbfcc51e..e7b3770e 100644 --- a/deploy/kubernetes/helm/sloth/tests/values_test.go +++ b/deploy/kubernetes/helm/sloth/tests/values_test.go @@ -8,11 +8,16 @@ func defaultValues() msi { func customValues() msi { return msi{ + "global": msi{ + "imageRegistry": "", + }, + "labels": msi{ "label-from": "test", }, "image": msi{ + "registry": "", "repository": "slok/sloth-test", "tag": "v1.42.42", }, From 1b1c583517511e5e35d66d3e20662b19d4b0fd71 Mon Sep 17 00:00:00 2001 From: QuantumEnigmaa Date: Mon, 31 Mar 2025 17:26:52 +0200 Subject: [PATCH 028/173] add comment in values and fix helm test Signed-off-by: QuantumEnigmaa --- deploy/kubernetes/helm/sloth/tests/values_test.go | 4 ++-- deploy/kubernetes/helm/sloth/values.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deploy/kubernetes/helm/sloth/tests/values_test.go b/deploy/kubernetes/helm/sloth/tests/values_test.go index e7b3770e..ec54feae 100644 --- a/deploy/kubernetes/helm/sloth/tests/values_test.go +++ b/deploy/kubernetes/helm/sloth/tests/values_test.go @@ -17,8 +17,8 @@ func customValues() msi { }, "image": msi{ - "registry": "", - "repository": "slok/sloth-test", + "registry": "slok", + "repository": "sloth-test", "tag": "v1.42.42", }, diff --git a/deploy/kubernetes/helm/sloth/values.yaml b/deploy/kubernetes/helm/sloth/values.yaml index d86815f3..194a925a 100644 --- a/deploy/kubernetes/helm/sloth/values.yaml +++ b/deploy/kubernetes/helm/sloth/values.yaml @@ -1,10 +1,10 @@ global: - imageRegistry: "" + imageRegistry: "" # This field cannot be empty if image.registry is also empty. labels: {} image: - registry: ghcr.io + registry: ghcr.io # This field cannot be empty if global.imageRegistry is also empty. repository: slok/sloth tag: v0.12.0 From eea9204e54bf470e74a40d24f461e734c48df652 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Mon, 31 Mar 2025 18:28:53 +0200 Subject: [PATCH 029/173] Move k8s YAML and CR loaders to io storage Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 18 +- cmd/sloth/commands/k8scontroller.go | 3 +- cmd/sloth/commands/validate.go | 3 +- internal/app/kubecontroller/handler.go | 18 +- internal/k8sprometheus/model.go | 44 ---- internal/k8sprometheus/model_test.go | 137 ---------- internal/k8sprometheus/storage.go | 11 + .../spec.go => storage/io/k8s_sloth.go} | 79 +++--- .../io/k8s_sloth_test.go} | 246 ++++++++++-------- internal/storage/io/openslo.go | 25 +- internal/storage/io/openslo_test.go | 52 +++- internal/storage/io/sloth.go | 5 +- internal/storage/io/sloth_test.go | 91 ++++++- pkg/common/model/slo_prometheus.go | 15 +- 14 files changed, 390 insertions(+), 357 deletions(-) delete mode 100644 internal/k8sprometheus/model.go delete mode 100644 internal/k8sprometheus/model_test.go rename internal/{k8sprometheus/spec.go => storage/io/k8s_sloth.go} (68%) rename internal/{k8sprometheus/spec_test.go => storage/io/k8s_sloth_test.go} (53%) diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 4568a8dd..123609d5 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -133,7 +133,7 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { // Create Spec loaders. promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) - kubeYAMLLoader := k8sprometheus.NewYAMLSpecLoader(pluginRepo, sloPeriod) + kubeYAMLLoader := storageio.NewK8sSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) openSLOYAMLLoader := storageio.NewOpenSLOYAMLSpecLoader(sloPeriod) // Get SLO targets. @@ -340,7 +340,7 @@ func (g generator) GeneratePrometheus(ctx context.Context, slos prometheus.SLOGr } // generateKubernetes generates the SLOs based on a Kuberentes spec format input and outs a Kubernetes prometheus operator CRD yaml. -func (g generator) GenerateKubernetes(ctx context.Context, sloGroup k8sprometheus.SLOGroup, out io.Writer) error { +func (g generator) GenerateKubernetes(ctx context.Context, sloGroup model.PromSLOGroup, out io.Writer) error { g.logger.Infof("Generating from Kubernetes Prometheus spec") info := model.Info{ @@ -348,7 +348,7 @@ func (g generator) GenerateKubernetes(ctx context.Context, sloGroup k8sprometheu Mode: model.ModeCLIGenKubernetes, Spec: fmt.Sprintf("%s/%s", kubernetesv1.SchemeGroupVersion.Group, kubernetesv1.SchemeGroupVersion.Version), } - result, err := g.generateRules(ctx, info, sloGroup.SLOGroup) + result, err := g.generateRules(ctx, info, sloGroup) if err != nil { return err } @@ -362,7 +362,17 @@ func (g generator) GenerateKubernetes(ctx context.Context, sloGroup k8sprometheu }) } - err = repo.StoreSLOs(ctx, sloGroup.K8sMeta, storageSLOs) + kmeta := k8sprometheus.K8sMeta{ + Kind: "PrometheusServiceLevel", + APIVersion: "sloth.slok.dev/v1", + UID: string(sloGroup.OriginalSource.K8sSlothV1.UID), + Name: sloGroup.OriginalSource.K8sSlothV1.Name, + Namespace: sloGroup.OriginalSource.K8sSlothV1.Namespace, + Labels: sloGroup.OriginalSource.K8sSlothV1.Labels, + Annotations: sloGroup.OriginalSource.K8sSlothV1.Annotations, + } + + err = repo.StoreSLOs(ctx, kmeta, storageSLOs) if err != nil { return fmt.Errorf("could not store SLOS: %w", err) } diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index 0b554d34..bb874654 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -36,6 +36,7 @@ import ( "github.com/slok/sloth/internal/k8sprometheus" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/prometheus" + storageio "github.com/slok/sloth/internal/storage/io" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientset "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" ) @@ -319,7 +320,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error // Create handler. config := kubecontroller.HandlerConfig{ Generator: generator, - SpecLoader: k8sprometheus.NewCRSpecLoader(pluginRepo, sloPeriod), + SpecLoader: storageio.NewK8sSlothPrometheusCRSpecLoader(pluginRepo, sloPeriod), Repository: k8sprometheus.NewPrometheusOperatorCRDRepo(ksvc, logger), KubeStatusStorer: ksvc, ExtraLabels: k.extraLabels, diff --git a/cmd/sloth/commands/validate.go b/cmd/sloth/commands/validate.go index 37e95f87..427254e8 100644 --- a/cmd/sloth/commands/validate.go +++ b/cmd/sloth/commands/validate.go @@ -13,7 +13,6 @@ import ( prometheusmodel "github.com/prometheus/common/model" "github.com/slok/sloth/internal/alert" - "github.com/slok/sloth/internal/k8sprometheus" "github.com/slok/sloth/internal/log" storageio "github.com/slok/sloth/internal/storage/io" ) @@ -108,7 +107,7 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { // Create Spec loaders. promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) - kubeYAMLLoader := k8sprometheus.NewYAMLSpecLoader(pluginRepo, sloPeriod) + kubeYAMLLoader := storageio.NewK8sSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) openSLOYAMLLoader := storageio.NewOpenSLOYAMLSpecLoader(sloPeriod) // For every file load the data and start the validation process: diff --git a/internal/app/kubecontroller/handler.go b/internal/app/kubecontroller/handler.go index c9afed09..fda903ae 100644 --- a/internal/app/kubecontroller/handler.go +++ b/internal/app/kubecontroller/handler.go @@ -12,13 +12,14 @@ import ( "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/k8sprometheus" "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/pkg/common/model" commonmodel "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" ) // SpecLoader Knows how to load a Kubernetes Spec into an app model. type SpecLoader interface { - LoadSpec(ctx context.Context, spec *slothv1.PrometheusServiceLevel) (*k8sprometheus.SLOGroup, error) + LoadSpec(ctx context.Context, spec *slothv1.PrometheusServiceLevel) (*model.PromSLOGroup, error) } // Generator Knows how to generate SLO prometheus rules from app SLO model. @@ -153,7 +154,7 @@ func (h handler) handlePrometheusServiceLevelV1(ctx context.Context, psl *slothv Spec: fmt.Sprintf("%s/%s", slothv1.SchemeGroupVersion.Group, slothv1.SchemeGroupVersion.Version), }, ExtraLabels: h.extraLabels, - SLOGroup: model.SLOGroup, + SLOGroup: *model, } resp, err := h.generator.Generate(ctx, req) if err != nil { @@ -168,7 +169,18 @@ func (h handler) handlePrometheusServiceLevelV1(ctx context.Context, psl *slothv Rules: s.SLORules, }) } - err = h.repository.StoreSLOs(ctx, model.K8sMeta, storageSLOs) + + kmeta := k8sprometheus.K8sMeta{ + Kind: "PrometheusServiceLevel", + APIVersion: "sloth.slok.dev/v1", + UID: string(model.OriginalSource.K8sSlothV1.UID), + Name: model.OriginalSource.K8sSlothV1.Name, + Namespace: model.OriginalSource.K8sSlothV1.Namespace, + Labels: model.OriginalSource.K8sSlothV1.Labels, + Annotations: model.OriginalSource.K8sSlothV1.Annotations, + } + + err = h.repository.StoreSLOs(ctx, kmeta, storageSLOs) if err != nil { return fmt.Errorf("could not store SLOs: %w", err) } diff --git a/internal/k8sprometheus/model.go b/internal/k8sprometheus/model.go deleted file mode 100644 index d338fabc..00000000 --- a/internal/k8sprometheus/model.go +++ /dev/null @@ -1,44 +0,0 @@ -package k8sprometheus - -import ( - "github.com/go-playground/validator/v10" - - "github.com/slok/sloth/internal/prometheus" -) - -// K8sMeta is the Kubernetes metadata simplified. -type K8sMeta struct { - Kind string `validate:"required"` - APIVersion string `validate:"required"` - Name string `validate:"required"` - UID string - Namespace string - Annotations map[string]string - Labels map[string]string -} - -// SLOGroup is a Kubernetes SLO group. Is created based on a regular Prometheus -// SLO model and Kubernetes data. -type SLOGroup struct { - K8sMeta K8sMeta - prometheus.SLOGroup -} - -// Validate validates the SLO. -func (s SLOGroup) Validate() error { - err := modelSpecValidate.Struct(s.K8sMeta) - if err != nil { - return err - } - - err = s.SLOGroup.Validate() - if err != nil { - return err - } - - return nil -} - -var modelSpecValidate = func() *validator.Validate { - return validator.New() -}() diff --git a/internal/k8sprometheus/model_test.go b/internal/k8sprometheus/model_test.go deleted file mode 100644 index dd9b87e2..00000000 --- a/internal/k8sprometheus/model_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package k8sprometheus_test - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/slok/sloth/internal/k8sprometheus" - "github.com/slok/sloth/internal/prometheus" -) - -func getGoodSLOGroup() k8sprometheus.SLOGroup { - return k8sprometheus.SLOGroup{ - K8sMeta: k8sprometheus.K8sMeta{ - Kind: "PrometheusServiceLevel", - APIVersion: "sloth.slok.dev/v1", - Name: "test", - Namespace: "test-ns", - }, - SLOGroup: prometheus.SLOGroup{SLOs: []prometheus.SLO{ - getGoodSLO("slo1"), - getGoodSLO("slo2"), - }, - }, - } -} - -func getGoodSLO(name string) prometheus.SLO { - return prometheus.SLO{ - ID: fmt.Sprintf("%s-id", name), - Name: name, - Service: "test-svc", - TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ - ErrorQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))`, - TotalQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp"}[{{ .window }}]))`, - }, - }, - Objective: 99.99, - Labels: map[string]string{ - "owner": "myteam", - "category": "test", - }, - PageAlertMeta: prometheus.AlertMeta{ - Disable: false, - Name: "testAlert", - Labels: map[string]string{ - "tier": "1", - "severity": "slack", - "channel": "#a-myteam", - }, - Annotations: map[string]string{ - "message": "This is very important.", - "runbook": "http://whatever.com", - }, - }, - TicketAlertMeta: prometheus.AlertMeta{ - Disable: false, - Name: "testAlert", - Labels: map[string]string{ - "tier": "1", - "severity": "slack", - "channel": "#a-not-so-important", - }, - Annotations: map[string]string{ - "message": "This is not very important.", - "runbook": "http://whatever.com", - }, - }, - } -} - -func TestModelValidationSpec(t *testing.T) { - tests := map[string]struct { - slos func() k8sprometheus.SLOGroup - expErrMessage string - }{ - "Correct SLOs should not fail.": { - slos: getGoodSLOGroup, - }, - - "Kind is required.": { - slos: func() k8sprometheus.SLOGroup { - sg := getGoodSLOGroup() - sg.K8sMeta.Kind = "" - return sg - }, - expErrMessage: "Key: 'K8sMeta.Kind' Error:Field validation for 'Kind' failed on the 'required' tag", - }, - - "APIVersion is required.": { - slos: func() k8sprometheus.SLOGroup { - sg := getGoodSLOGroup() - sg.K8sMeta.APIVersion = "" - return sg - }, - expErrMessage: "Key: 'K8sMeta.APIVersion' Error:Field validation for 'APIVersion' failed on the 'required' tag", - }, - - "Name is required.": { - slos: func() k8sprometheus.SLOGroup { - sg := getGoodSLOGroup() - sg.K8sMeta.Name = "" - return sg - }, - expErrMessage: "Key: 'K8sMeta.Name' Error:Field validation for 'Name' failed on the 'required' tag", - }, - - "SLO validation is execute correctly and fails if SLOs fail.": { - slos: func() k8sprometheus.SLOGroup { - sg := getGoodSLOGroup() - sg.SLOs[0].ID = "" - return sg - }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'required' tag", - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - - slos := test.slos() - err := slos.Validate() - - if test.expErrMessage != "" { - assert.Error(err) - assert.Equal(test.expErrMessage, err.Error()) - } else { - assert.NoError(err) - } - }) - } -} diff --git a/internal/k8sprometheus/storage.go b/internal/k8sprometheus/storage.go index 8527d514..85559ee0 100644 --- a/internal/k8sprometheus/storage.go +++ b/internal/k8sprometheus/storage.go @@ -19,6 +19,17 @@ import ( "github.com/slok/sloth/internal/prometheus" ) +// K8sMeta is the Kubernetes metadata simplified. +type K8sMeta struct { + Kind string `validate:"required"` + APIVersion string `validate:"required"` + Name string `validate:"required"` + UID string + Namespace string + Annotations map[string]string + Labels map[string]string +} + var ( // ErrNoSLORules will be used when there are no rules to store. The upper layer // could ignore or handle the error in cases where there wasn't an output. diff --git a/internal/k8sprometheus/spec.go b/internal/storage/io/k8s_sloth.go similarity index 68% rename from internal/k8sprometheus/spec.go rename to internal/storage/io/k8s_sloth.go index ba0e62a3..154fd7f6 100644 --- a/internal/k8sprometheus/spec.go +++ b/internal/storage/io/k8s_sloth.go @@ -1,4 +1,4 @@ -package k8sprometheus +package io import ( "context" @@ -8,28 +8,41 @@ import ( "k8s.io/apimachinery/pkg/runtime" - pluginsli "github.com/slok/sloth/internal/plugin/sli" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" k8sprometheusv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/scheme" prometheuspluginv1 "github.com/slok/sloth/pkg/prometheus/plugin/v1" ) -type SLIPluginRepo interface { - GetSLIPlugin(ctx context.Context, id string) (*pluginsli.SLIPlugin, error) +type K8sSlothPrometheusCRSpecLoader struct { + windowPeriod time.Duration + pluginsRepo SLIPluginRepo +} + +// NewK8sSlothPrometheusCRSpecLoader knows how to load Kubernetes CRD specs and converts them to a model. +func NewK8sSlothPrometheusCRSpecLoader(pluginsRepo SLIPluginRepo, windowPeriod time.Duration) K8sSlothPrometheusCRSpecLoader { + return K8sSlothPrometheusCRSpecLoader{ + windowPeriod: windowPeriod, + pluginsRepo: pluginsRepo, + } +} + +func (c K8sSlothPrometheusCRSpecLoader) LoadSpec(ctx context.Context, spec *k8sprometheusv1.PrometheusServiceLevel) (*model.PromSLOGroup, error) { + return mapSpecToModel(ctx, c.windowPeriod, c.pluginsRepo, spec) } -// YAMLSpecLoader knows how to load Kubernetes ServiceLevel YAML specs and converts them to a model. -type YAMLSpecLoader struct { +// K8sSlothPrometheusYAMLSpecLoader knows how to load Kubernetes ServiceLevel YAML specs and converts them to a model. +type K8sSlothPrometheusYAMLSpecLoader struct { windowPeriod time.Duration pluginsRepo SLIPluginRepo decoder runtime.Decoder } -// NewYAMLSpecLoader returns a YAML spec loader. -func NewYAMLSpecLoader(pluginsRepo SLIPluginRepo, windowPeriod time.Duration) YAMLSpecLoader { - return YAMLSpecLoader{ +// NewK8sSlothPrometheusYAMLSpecLoader returns a YAML spec loader. +func NewK8sSlothPrometheusYAMLSpecLoader(pluginsRepo SLIPluginRepo, windowPeriod time.Duration) K8sSlothPrometheusYAMLSpecLoader { + return K8sSlothPrometheusYAMLSpecLoader{ windowPeriod: windowPeriod, pluginsRepo: pluginsRepo, decoder: scheme.Codecs.UniversalDeserializer(), @@ -37,20 +50,20 @@ func NewYAMLSpecLoader(pluginsRepo SLIPluginRepo, windowPeriod time.Duration) YA } var ( - specTypeV1RegexKind = regexp.MustCompile(`(?m)^kind: +['"]?PrometheusServiceLevel['"]? *$`) - specTypeV1RegexAPIVersion = regexp.MustCompile(`(?m)^apiVersion: +['"]?sloth.slok.dev\/v1['"]? *$`) + k8sSlothPromSpecTypeV1RegexKind = regexp.MustCompile(`(?m)^kind: +['"]?PrometheusServiceLevel['"]? *$`) + k8sSlothPromSpecTypeV1RegexAPIVersion = regexp.MustCompile(`(?m)^apiVersion: +['"]?sloth.slok.dev\/v1['"]? *$`) ) -func (y YAMLSpecLoader) IsSpecType(ctx context.Context, data []byte) bool { - return specTypeV1RegexKind.Match(data) && specTypeV1RegexAPIVersion.Match(data) +func (l K8sSlothPrometheusYAMLSpecLoader) IsSpecType(ctx context.Context, data []byte) bool { + return k8sSlothPromSpecTypeV1RegexKind.Match(data) && k8sSlothPromSpecTypeV1RegexAPIVersion.Match(data) } -func (y YAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*SLOGroup, error) { +func (l K8sSlothPrometheusYAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*model.PromSLOGroup, error) { if len(data) == 0 { return nil, fmt.Errorf("spec is required") } - obj, _, err := y.decoder.Decode([]byte(data), nil, nil) + obj, _, err := l.decoder.Decode([]byte(data), nil, nil) if err != nil { return nil, fmt.Errorf("could not decode kubernetes object %w", err) } @@ -65,7 +78,7 @@ func (y YAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*SLOGroup, e return nil, fmt.Errorf("at least one SLO is required") } - m, err := mapSpecToModel(ctx, y.windowPeriod, y.pluginsRepo, kslo) + m, err := mapSpecToModel(ctx, l.windowPeriod, l.pluginsRepo, kslo) if err != nil { return nil, fmt.Errorf("could not map to model: %w", err) } @@ -73,25 +86,7 @@ func (y YAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*SLOGroup, e return m, nil } -type CRSpecLoader struct { - windowPeriod time.Duration - pluginsRepo SLIPluginRepo -} - -// CRSpecLoader knows how to load Kubernetes CRD specs and converts them to a model. - -func NewCRSpecLoader(pluginsRepo SLIPluginRepo, windowPeriod time.Duration) CRSpecLoader { - return CRSpecLoader{ - windowPeriod: windowPeriod, - pluginsRepo: pluginsRepo, - } -} - -func (c CRSpecLoader) LoadSpec(ctx context.Context, spec *k8sprometheusv1.PrometheusServiceLevel) (*SLOGroup, error) { - return mapSpecToModel(ctx, c.windowPeriod, c.pluginsRepo, spec) -} - -func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, pluginsRepo SLIPluginRepo, kspec *k8sprometheusv1.PrometheusServiceLevel) (*SLOGroup, error) { +func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, pluginsRepo SLIPluginRepo, kspec *k8sprometheusv1.PrometheusServiceLevel) (*model.PromSLOGroup, error) { slos := make([]prometheus.SLO, 0, len(kspec.Spec.SLOs)) spec := kspec.Spec for _, specSLO := range kspec.Spec.SLOs { @@ -163,17 +158,11 @@ func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, plug slos = append(slos, slo) } - res := &SLOGroup{ - K8sMeta: K8sMeta{ - Kind: "PrometheusServiceLevel", - APIVersion: "sloth.slok.dev/v1", - UID: string(kspec.UID), - Name: kspec.Name, - Namespace: kspec.Namespace, - Labels: kspec.Labels, - Annotations: kspec.Annotations, + res := &model.PromSLOGroup{ + SLOs: slos, + OriginalSource: model.PromSLOGroupSource{ + K8sSlothV1: kspec, }, - SLOGroup: prometheus.SLOGroup{SLOs: slos}, } return res, nil diff --git a/internal/k8sprometheus/spec_test.go b/internal/storage/io/k8s_sloth_test.go similarity index 53% rename from internal/k8sprometheus/spec_test.go rename to internal/storage/io/k8s_sloth_test.go index 8db0ea62..70bb84c0 100644 --- a/internal/k8sprometheus/spec_test.go +++ b/internal/storage/io/k8s_sloth_test.go @@ -1,4 +1,4 @@ -package k8sprometheus_test +package io_test import ( "context" @@ -7,27 +7,20 @@ import ( "time" "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/slok/sloth/internal/k8sprometheus" pluginsli "github.com/slok/sloth/internal/plugin/sli" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/internal/storage/io" + "github.com/slok/sloth/pkg/common/model" + kubeslothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" ) -type testMemPluginsRepo map[string]pluginsli.SLIPlugin - -func (t testMemPluginsRepo) GetSLIPlugin(ctx context.Context, id string) (*pluginsli.SLIPlugin, error) { - p, ok := t[id] - if !ok { - return nil, fmt.Errorf("unknown plugin") - } - return &p, nil -} - -func TestYAMLoadSpec(t *testing.T) { +func TestK8sSlothPrometheusYAMLSpecLoader(t *testing.T) { tests := map[string]struct { specYaml string plugins map[string]pluginsli.SLIPlugin - expModel *k8sprometheus.SLOGroup + expModel *model.PromSLOGroup expErr bool }{ "Empty spec should fail.": { @@ -154,29 +147,44 @@ spec: ticketAlert: disable: true `, - expModel: &k8sprometheus.SLOGroup{ - K8sMeta: k8sprometheus.K8sMeta{ - Kind: "PrometheusServiceLevel", - APIVersion: "sloth.slok.dev/v1", - UID: "", - Name: "k8s-test-svc", - Namespace: "test-ns", + expModel: &model.PromSLOGroup{SLOs: []prometheus.SLO{ + { + ID: "test-svc-slo-test", + Name: "slo-test", + Service: "test-svc", + TimeWindow: 30 * 24 * time.Hour, + Labels: map[string]string{"gk1": "gv1"}, + SLI: prometheus.SLI{ + Raw: &prometheus.SLIRaw{ + ErrorRatioQuery: `plugin_raw_expr{service="test-svc",slo="slo-test",objective="99.000000",gk1="gv1",k1="v1",k2="true"}`, + }, + }, + Objective: 99, + PageAlertMeta: prometheus.AlertMeta{Disable: true}, + TicketAlertMeta: prometheus.AlertMeta{Disable: true}, }, - SLOGroup: prometheus.SLOGroup{SLOs: []prometheus.SLO{ - { - ID: "test-svc-slo-test", - Name: "slo-test", - Service: "test-svc", - TimeWindow: 30 * 24 * time.Hour, - Labels: map[string]string{"gk1": "gv1"}, - SLI: prometheus.SLI{ - Raw: &prometheus.SLIRaw{ - ErrorRatioQuery: `plugin_raw_expr{service="test-svc",slo="slo-test",objective="99.000000",gk1="gv1",k1="v1",k2="true"}`, + }, + OriginalSource: model.PromSLOGroupSource{K8sSlothV1: &kubeslothv1.PrometheusServiceLevel{ + TypeMeta: metav1.TypeMeta{Kind: "PrometheusServiceLevel", APIVersion: "sloth.slok.dev/v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "k8s-test-svc", + Namespace: "test-ns", + }, + Spec: kubeslothv1.PrometheusServiceLevelSpec{ + Service: "test-svc", + Labels: map[string]string{"gk1": "gv1"}, + SLOs: []kubeslothv1.SLO{ + {Name: "slo-test", Description: "", Objective: 99, + SLI: kubeslothv1.SLI{Plugin: &kubeslothv1.SLIPlugin{ID: "test_plugin", Options: map[string]string{ + "k1": "v1", + "k2": "true", + }}}, + Alerting: kubeslothv1.Alerting{ + PageAlert: kubeslothv1.Alert{Disable: true}, + TicketAlert: kubeslothv1.Alert{Disable: true}, + }, }, }, - Objective: 99, - PageAlertMeta: prometheus.AlertMeta{Disable: true}, - TicketAlertMeta: prometheus.AlertMeta{Disable: true}, }, }}, }, @@ -272,82 +280,112 @@ spec: ticketAlert: disable: true `, - expModel: &k8sprometheus.SLOGroup{ - K8sMeta: k8sprometheus.K8sMeta{ - Kind: "PrometheusServiceLevel", - APIVersion: "sloth.slok.dev/v1", - UID: "", - Name: "k8s-test-svc", - Namespace: "test-ns", - Labels: map[string]string{"lk1": "lv1", "lk2": "lv2"}, - Annotations: map[string]string{"ak1": "av1", "ak2": "av2"}, - }, - SLOGroup: prometheus.SLOGroup{SLOs: []prometheus.SLO{ - { - ID: "test-svc-slo1", - Name: "slo1", - Description: "This is a test.", - Service: "test-svc", - TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ - ErrorQuery: "test_expr_error_1", - TotalQuery: "test_expr_total_1", - }, + expModel: &model.PromSLOGroup{SLOs: []prometheus.SLO{ + { + ID: "test-svc-slo1", + Name: "slo1", + Description: "This is a test.", + Service: "test-svc", + TimeWindow: 30 * 24 * time.Hour, + SLI: prometheus.SLI{ + Events: &prometheus.SLIEvents{ + ErrorQuery: "test_expr_error_1", + TotalQuery: "test_expr_total_1", }, - Objective: 99.99999, + }, + Objective: 99.99999, + Labels: map[string]string{ + "owner": "myteam", + "category": "test", + }, + PageAlertMeta: prometheus.AlertMeta{ + Disable: false, + Name: "testAlert", Labels: map[string]string{ - "owner": "myteam", - "category": "test", - }, - PageAlertMeta: prometheus.AlertMeta{ - Disable: false, - Name: "testAlert", - Labels: map[string]string{ - "tier": "1", - "severity": "slack", - "channel": "#a-myteam", - }, - Annotations: map[string]string{ - "message": "This is very important.", - "runbook": "http://whatever.com", - }, + "tier": "1", + "severity": "slack", + "channel": "#a-myteam", }, - TicketAlertMeta: prometheus.AlertMeta{ - Disable: false, - Name: "testAlert", - Labels: map[string]string{ - "tier": "1", - "severity": "slack", - "channel": "#a-not-so-important", - }, - Annotations: map[string]string{ - "message": "This is not very important.", - "runbook": "http://whatever.com", - }, + Annotations: map[string]string{ + "message": "This is very important.", + "runbook": "http://whatever.com", }, }, - { - ID: "test-svc-slo2", - Name: "slo2", - Service: "test-svc", - TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Raw: &prometheus.SLIRaw{ - ErrorRatioQuery: "test_expr_ratio_2", - }, - }, - Objective: 99.9, + TicketAlertMeta: prometheus.AlertMeta{ + Disable: false, + Name: "testAlert", Labels: map[string]string{ - "owner": "myteam", - "category": "test2", + "tier": "1", + "severity": "slack", + "channel": "#a-not-so-important", + }, + Annotations: map[string]string{ + "message": "This is not very important.", + "runbook": "http://whatever.com", }, - PageAlertMeta: prometheus.AlertMeta{Disable: true}, - TicketAlertMeta: prometheus.AlertMeta{Disable: true}, }, }, + { + ID: "test-svc-slo2", + Name: "slo2", + Service: "test-svc", + TimeWindow: 30 * 24 * time.Hour, + SLI: prometheus.SLI{ + Raw: &prometheus.SLIRaw{ + ErrorRatioQuery: "test_expr_ratio_2", + }, + }, + Objective: 99.9, + Labels: map[string]string{ + "owner": "myteam", + "category": "test2", + }, + PageAlertMeta: prometheus.AlertMeta{Disable: true}, + TicketAlertMeta: prometheus.AlertMeta{Disable: true}, }, }, + OriginalSource: model.PromSLOGroupSource{K8sSlothV1: &kubeslothv1.PrometheusServiceLevel{ + TypeMeta: metav1.TypeMeta{Kind: "PrometheusServiceLevel", APIVersion: "sloth.slok.dev/v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "k8s-test-svc", + Namespace: "test-ns", + Labels: map[string]string{"lk1": "lv1", "lk2": "lv2"}, + Annotations: map[string]string{"ak1": "av1", "ak2": "av2"}, + }, + Spec: kubeslothv1.PrometheusServiceLevelSpec{ + Service: "test-svc", + Labels: map[string]string{"owner": "myteam"}, + SLOs: []kubeslothv1.SLO{ + {Name: "slo1", Description: "This is a test.", Objective: 99.99999, Labels: map[string]string{"category": "test"}, + SLI: kubeslothv1.SLI{Events: &kubeslothv1.SLIEvents{ + ErrorQuery: "test_expr_error_1", + TotalQuery: "test_expr_total_1", + }}, + Alerting: kubeslothv1.Alerting{ + Name: "testAlert", + Labels: map[string]string{"tier": "1"}, + Annotations: map[string]string{"runbook": "http://whatever.com"}, + PageAlert: kubeslothv1.Alert{ + Labels: map[string]string{"channel": "#a-myteam", "severity": "slack"}, + Annotations: map[string]string{"message": "This is very important."}, + }, + TicketAlert: kubeslothv1.Alert{ + Labels: map[string]string{"channel": "#a-not-so-important", "severity": "slack"}, + Annotations: map[string]string{"message": "This is not very important."}, + }, + }, + }, + {Name: "slo2", Objective: 99.9, Labels: map[string]string{"category": "test2"}, + SLI: kubeslothv1.SLI{Raw: &kubeslothv1.SLIRaw{ErrorRatioQuery: "test_expr_ratio_2"}}, + Alerting: kubeslothv1.Alerting{ + PageAlert: kubeslothv1.Alert{Disable: true}, + TicketAlert: kubeslothv1.Alert{Disable: true}, + }, + }, + }, + }, + }}, + }, }, } @@ -355,7 +393,7 @@ spec: t.Run(name, func(t *testing.T) { assert := assert.New(t) - loader := k8sprometheus.NewYAMLSpecLoader(testMemPluginsRepo(test.plugins), 30*24*time.Hour) + loader := io.NewK8sSlothPrometheusYAMLSpecLoader(testMemPluginsRepo(test.plugins), 30*24*time.Hour) gotModel, err := loader.LoadSpec(context.TODO(), []byte(test.specYaml)) if test.expErr { @@ -367,7 +405,7 @@ spec: } } -func TestYAMLIsSpecType(t *testing.T) { +func TestK8sSlothPrometheusYAMLSpecLoadeIsSpecType(t *testing.T) { tests := map[string]struct { specYaml string exp bool @@ -424,8 +462,8 @@ kind: 'PrometheusServiceLevel' "An correct spec type should match (multiple spaces)": { specYaml: ` -apiVersion: sloth.slok.dev/v1 -kind: PrometheusServiceLevel +apiVersion: sloth.slok.dev/v1 +kind: PrometheusServiceLevel `, exp: true, }, @@ -435,7 +473,7 @@ kind: PrometheusServiceLevel t.Run(name, func(t *testing.T) { assert := assert.New(t) - loader := k8sprometheus.NewYAMLSpecLoader(testMemPluginsRepo(map[string]pluginsli.SLIPlugin{}), 30*24*time.Hour) + loader := io.NewK8sSlothPrometheusYAMLSpecLoader(testMemPluginsRepo(map[string]pluginsli.SLIPlugin{}), 30*24*time.Hour) got := loader.IsSpecType(context.TODO(), []byte(test.specYaml)) assert.Equal(test.exp, got) diff --git a/internal/storage/io/openslo.go b/internal/storage/io/openslo.go index 7e5c3762..7631dd2a 100644 --- a/internal/storage/io/openslo.go +++ b/internal/storage/io/openslo.go @@ -12,7 +12,7 @@ import ( openslov1alpha "github.com/OpenSLO/oslo/pkg/manifest/v1alpha" "gopkg.in/yaml.v2" - "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/pkg/common/model" ) type OpenSLOYAMLSpecLoader struct { @@ -35,7 +35,7 @@ func (l OpenSLOYAMLSpecLoader) IsSpecType(ctx context.Context, data []byte) bool return openSLOSpecV1AlphaTypeRegexKind.Match(data) && openSLOSpecV1AlphaTypeRegexAPIVersion.Match(data) } -func (l OpenSLOYAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*prometheus.SLOGroup, error) { +func (l OpenSLOYAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*model.PromSLOGroup, error) { if len(data) == 0 { return nil, fmt.Errorf("spec is required") } @@ -70,13 +70,16 @@ func (l OpenSLOYAMLSpecLoader) LoadSpec(ctx context.Context, data []byte) (*prom return m, nil } -func (l OpenSLOYAMLSpecLoader) mapSpecToModel(spec openslov1alpha.SLO) (*prometheus.SLOGroup, error) { +func (l OpenSLOYAMLSpecLoader) mapSpecToModel(spec openslov1alpha.SLO) (*model.PromSLOGroup, error) { slos, err := l.getSLOs(spec) if err != nil { return nil, fmt.Errorf("could not map SLOs correctly: %w", err) } - return &prometheus.SLOGroup{SLOs: slos}, nil + return &model.PromSLOGroup{ + SLOs: slos, + OriginalSource: model.PromSLOGroupSource{OpenSLOV1Alpha: &spec}, + }, nil } // validateTimeWindow will validate that Sloth only supports 30 day based time windows @@ -114,7 +117,7 @@ var openSLOErrorRatioRawQueryTpl = template.Must(template.New("").Parse(` // however we will convert to a raw based sloth SLI because the ratio queries that we have differ from // Sloth. Sloth uses bad/total events, OpenSLO uses good/total events. We get the ratio using good events // and then rest to 1, to get a raw error ratio query. -func (l OpenSLOYAMLSpecLoader) getSLI(spec openslov1alpha.SLOSpec, slo openslov1alpha.Objective) (*prometheus.SLI, error) { +func (l OpenSLOYAMLSpecLoader) getSLI(spec openslov1alpha.SLOSpec, slo openslov1alpha.Objective) (*model.PromSLI, error) { if slo.RatioMetrics == nil { return nil, fmt.Errorf("missing ratioMetrics") } @@ -145,7 +148,7 @@ func (l OpenSLOYAMLSpecLoader) getSLI(spec openslov1alpha.SLOSpec, slo openslov1 return nil, fmt.Errorf("could not execute mapping SLI template: %w", err) } - return &prometheus.SLI{Raw: &prometheus.SLIRaw{ + return &model.PromSLI{Raw: &model.PromSLIRaw{ ErrorRatioQuery: b.String(), }}, nil } @@ -153,8 +156,8 @@ func (l OpenSLOYAMLSpecLoader) getSLI(spec openslov1alpha.SLOSpec, slo openslov1 // getSLOs will try getting all the objectives as individual SLOs, this way we can map // to what Sloth understands as an SLO, that OpenSLO understands as a list of objectives // for the same SLO. -func (l OpenSLOYAMLSpecLoader) getSLOs(spec openslov1alpha.SLO) ([]prometheus.SLO, error) { - res := []prometheus.SLO{} +func (l OpenSLOYAMLSpecLoader) getSLOs(spec openslov1alpha.SLO) ([]model.PromSLO, error) { + res := []model.PromSLO{} for idx, slo := range spec.Spec.Objectives { sli, err := l.getSLI(spec.Spec, slo) @@ -168,7 +171,7 @@ func (l OpenSLOYAMLSpecLoader) getSLOs(spec openslov1alpha.SLO) ([]prometheus.SL } // TODO(slok): Think about using `slo.Value` insted of idx (`slo.Value` is not mandatory). - res = append(res, prometheus.SLO{ + res = append(res, model.PromSLO{ ID: fmt.Sprintf("%s-%s-%d", spec.Spec.Service, spec.Metadata.Name, idx), Name: fmt.Sprintf("%s-%d", spec.Metadata.Name, idx), Service: spec.Spec.Service, @@ -176,8 +179,8 @@ func (l OpenSLOYAMLSpecLoader) getSLOs(spec openslov1alpha.SLO) ([]prometheus.SL TimeWindow: timeWindow, SLI: *sli, Objective: *slo.BudgetTarget * 100, // OpenSLO uses ratios, we use percents. - PageAlertMeta: prometheus.AlertMeta{Disable: true}, - TicketAlertMeta: prometheus.AlertMeta{Disable: true}, + PageAlertMeta: model.PromAlertMeta{Disable: true}, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, }) } diff --git a/internal/storage/io/openslo_test.go b/internal/storage/io/openslo_test.go index dac88c3b..10452888 100644 --- a/internal/storage/io/openslo_test.go +++ b/internal/storage/io/openslo_test.go @@ -5,10 +5,13 @@ import ( "testing" "time" + "github.com/OpenSLO/oslo/pkg/manifest" + "github.com/OpenSLO/oslo/pkg/manifest/v1alpha" "github.com/stretchr/testify/assert" "github.com/slok/sloth/internal/prometheus" "github.com/slok/sloth/internal/storage/io" + "github.com/slok/sloth/pkg/common/model" ) func TestOpenSLOYAMLSpecLoader(t *testing.T) { @@ -269,7 +272,54 @@ spec: PageAlertMeta: prometheus.AlertMeta{Disable: true}, TicketAlertMeta: prometheus.AlertMeta{Disable: true}, }, - }}, + }, + OriginalSource: model.PromSLOGroupSource{OpenSLOV1Alpha: &v1alpha.SLO{ + ObjectHeader: v1alpha.ObjectHeader{ + ObjectHeader: manifest.ObjectHeader{APIVersion: "openslo/v1alpha"}, + Kind: "SLO", + MetadataHolder: v1alpha.MetadataHolder{Metadata: v1alpha.Metadata{Name: "ratio", DisplayName: "Ratio"}}}, + Spec: v1alpha.SLOSpec{ + TimeWindows: []v1alpha.TimeWindow{{Unit: "Day", Count: 28, IsRolling: true}}, + BudgetingMethod: "Timeslices", + Description: "A great description of a ratio based SLO", + Service: "my-test-service", + Objectives: []v1alpha.Objective{ + { + ObjectiveBase: v1alpha.ObjectiveBase{DisplayName: "painful"}, + RatioMetrics: &v1alpha.RatioMetrics{ + Good: v1alpha.MetricSourceSpec{ + Source: "prometheus", + QueryType: "promql", + Query: `latency_west_c7{code="GOOD",instance="localhost:3000",job="prometheus",service="globacount"}`, + }, + Total: v1alpha.MetricSourceSpec{ + Source: "prometheus", + QueryType: "promql", + Query: `latency_west_c7{code="ALL",instance="localhost:3000",job="prometheus",service="globacount"}`, + }, + }, + BudgetTarget: &[]float64{0.98}[0], + }, + { + ObjectiveBase: v1alpha.ObjectiveBase{DisplayName: "painful"}, + RatioMetrics: &v1alpha.RatioMetrics{ + Good: v1alpha.MetricSourceSpec{ + Source: "prometheus", + QueryType: "promql", + Query: `latency_west_c7{code="GOOD",instance="localhost:3000",job="prometheus",service="globacount"}`, + }, + Total: v1alpha.MetricSourceSpec{ + Source: "prometheus", + QueryType: "promql", + Query: `latency_west_c7{code="ALL",instance="localhost:3000",job="prometheus",service="globacount"}`, + }, + }, + BudgetTarget: &[]float64{0.999}[0], + }, + }, + }, + }}, + }, }, } diff --git a/internal/storage/io/sloth.go b/internal/storage/io/sloth.go index e3ca96a7..e4220cef 100644 --- a/internal/storage/io/sloth.go +++ b/internal/storage/io/sloth.go @@ -139,5 +139,8 @@ func (l SlothPrometheusYAMLSpecLoader) mapSpecToModel(ctx context.Context, spec models = append(models, slo) } - return &model.PromSLOGroup{SLOs: models}, nil + return &model.PromSLOGroup{ + SLOs: models, + OriginalSource: model.PromSLOGroupSource{SlothV1: &spec}, + }, nil } diff --git a/internal/storage/io/sloth_test.go b/internal/storage/io/sloth_test.go index 0a08ead6..876bb9cc 100644 --- a/internal/storage/io/sloth_test.go +++ b/internal/storage/io/sloth_test.go @@ -11,6 +11,8 @@ import ( pluginsli "github.com/slok/sloth/internal/plugin/sli" "github.com/slok/sloth/internal/prometheus" "github.com/slok/sloth/internal/storage/io" + "github.com/slok/sloth/pkg/common/model" + v1 "github.com/slok/sloth/pkg/prometheus/api/v1" ) type testMemPluginsRepo map[string]pluginsli.SLIPlugin @@ -167,7 +169,27 @@ slos: PageAlertMeta: prometheus.AlertMeta{Disable: true}, TicketAlertMeta: prometheus.AlertMeta{Disable: true}, }, - }}, + }, + OriginalSource: model.PromSLOGroupSource{SlothV1: &v1.Spec{ + Version: "prometheus/v1", + Service: "test-svc", + Labels: map[string]string{"gk1": "gv1"}, + SLOs: []v1.SLO{ + { + Name: "slo-test", + Objective: 99, + SLI: v1.SLI{Plugin: &v1.SLIPlugin{ID: "test_plugin", Options: map[string]string{ + "k1": "v1", + "k2": "true", + }}}, + Alerting: v1.Alerting{ + PageAlert: v1.Alert{Disable: true}, + TicketAlert: v1.Alert{Disable: true}, + }, + }, + }, + }}, + }, }, "Spec with different time window should use the specific time window.": { @@ -205,7 +227,24 @@ slos: PageAlertMeta: prometheus.AlertMeta{Disable: true}, TicketAlertMeta: prometheus.AlertMeta{Disable: true}, }, - }}, + }, + OriginalSource: model.PromSLOGroupSource{SlothV1: &v1.Spec{ + Version: "prometheus/v1", + Service: "test-svc", + Labels: map[string]string{"gk1": "gv1"}, + SLOs: []v1.SLO{ + { + Name: "slo-test", + Objective: 99, + SLI: v1.SLI{Raw: &v1.SLIRaw{ErrorRatioQuery: "test_expr_ratio_2"}}, + Alerting: v1.Alerting{Name: "", + PageAlert: v1.Alert{Disable: true}, + TicketAlert: v1.Alert{Disable: true}, + }, + }, + }, + }}, + }, }, "Correct spec should return the models correctly.": { @@ -319,7 +358,53 @@ slos: PageAlertMeta: prometheus.AlertMeta{Disable: true}, TicketAlertMeta: prometheus.AlertMeta{Disable: true}, }, - }}, + }, + OriginalSource: model.PromSLOGroupSource{SlothV1: &v1.Spec{ + Version: "prometheus/v1", + Service: "test-svc", + Labels: map[string]string{"owner": "myteam"}, + SLOs: []v1.SLO{ + { + Name: "slo1", + Description: "This is a test.", + Objective: 99.99, + Labels: map[string]string{ + "category": "test", + }, + SLI: v1.SLI{ + Events: &v1.SLIEvents{ + ErrorQuery: `test_expr_error_1`, + TotalQuery: `test_expr_total_1`, + }, + }, + Alerting: v1.Alerting{Name: "testAlert", + Labels: map[string]string{"tier": "1"}, + Annotations: map[string]string{"runbook": "http://whatever.com"}, + PageAlert: v1.Alert{ + Labels: map[string]string{"channel": "#a-myteam", "severity": "slack"}, + Annotations: map[string]string{"message": "This is very important."}, + }, + TicketAlert: v1.Alert{ + Labels: map[string]string{"channel": "#a-not-so-important", "severity": "slack"}, + Annotations: map[string]string{"message": "This is not very important."}, + }, + }, + }, + { + Name: "slo2", + Objective: 99.9, + Labels: map[string]string{ + "category": "test2", + }, + SLI: v1.SLI{Raw: &v1.SLIRaw{ErrorRatioQuery: "test_expr_ratio_2"}}, + Alerting: v1.Alerting{Name: "", + PageAlert: v1.Alert{Disable: true}, + TicketAlert: v1.Alert{Disable: true}, + }, + }, + }, + }}, + }, }, } diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index 704ce7cb..5f3bc1e5 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -8,10 +8,14 @@ import ( "text/template" "time" + openslov1alpha "github.com/OpenSLO/oslo/pkg/manifest/v1alpha" "github.com/go-playground/validator/v10" prommodel "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/rulefmt" promqlparser "github.com/prometheus/prometheus/promql/parser" + + k8sprometheusv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" + prometheusv1 "github.com/slok/sloth/pkg/prometheus/api/v1" ) // SLI represents an SLI with custom error and total expressions. @@ -52,7 +56,16 @@ type PromSLO struct { } type PromSLOGroup struct { - SLOs []PromSLO `validate:"required,dive"` + SLOs []PromSLO `validate:"required,dive"` + OriginalSource PromSLOGroupSource +} + +// Used to store the original source of the SLO group in case we need to make low-level decision +// based on where the SLOs came from. +type PromSLOGroupSource struct { + K8sSlothV1 *k8sprometheusv1.PrometheusServiceLevel + SlothV1 *prometheusv1.Spec + OpenSLOV1Alpha *openslov1alpha.SLO } // Validate validates the SLO. From 07a4d02027ed363abd8dd52c31f760d7331c7a8e Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Mon, 31 Mar 2025 19:22:11 +0200 Subject: [PATCH 030/173] Add a middle domain model for prom rules Signed-off-by: Xabier Larrakoetxea --- internal/app/generate/prometheus.go | 14 ++--- internal/app/generate/prometheus_test.go | 38 ++++++------- internal/k8sprometheus/storage.go | 12 ++-- internal/k8sprometheus/storage_test.go | 65 +++++++++++----------- internal/storage/io/std_prometheus.go | 12 ++-- internal/storage/io/std_prometheus_test.go | 36 ++++++------ pkg/common/model/slo_prometheus.go | 11 +++- 7 files changed, 96 insertions(+), 92 deletions(-) diff --git a/internal/app/generate/prometheus.go b/internal/app/generate/prometheus.go index fd0e2976..d8a7b588 100644 --- a/internal/app/generate/prometheus.go +++ b/internal/app/generate/prometheus.go @@ -98,13 +98,13 @@ type Request struct { // ExtraLabels are the extra labels added to the SLOs on execution time. ExtraLabels map[string]string // SLOGroup are the SLOs group that will be used to generate the SLO results and Prom rules. - SLOGroup prometheus.SLOGroup + SLOGroup model.PromSLOGroup } type SLOResult struct { - SLO prometheus.SLO + SLO model.PromSLO Alerts model.MWMBAlertGroup - SLORules prometheus.SLORules + SLORules model.PromSLORules } type Response struct { @@ -176,10 +176,10 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, slo prometheu return &SLOResult{ SLO: slo, Alerts: *as, - SLORules: prometheus.SLORules{ - SLIErrorRecRules: sliRecordingRules, - MetadataRecRules: metaRecordingRules, - AlertRules: alertRules, + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: sliRecordingRules}, + MetadataRecRules: model.PromRuleGroup{Rules: metaRecordingRules}, + AlertRules: model.PromRuleGroup{Rules: alertRules}, }, }, nil } diff --git a/internal/app/generate/prometheus_test.go b/internal/app/generate/prometheus_test.go index 0d72e66e..c9db36c1 100644 --- a/internal/app/generate/prometheus_test.go +++ b/internal/app/generate/prometheus_test.go @@ -11,7 +11,6 @@ import ( "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/app/generate" - "github.com/slok/sloth/internal/prometheus" "github.com/slok/sloth/pkg/common/model" ) @@ -37,13 +36,13 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { Mode: model.ModeTest, Spec: "test-spec", }, - SLOGroup: prometheus.SLOGroup{SLOs: []prometheus.SLO{ + SLOGroup: model.PromSLOGroup{SLOs: []model.PromSLO{ { ID: "test-id", Name: "test-name", Service: "test-svc", - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ ErrorQuery: `rate(my_metric{error="true"}[{{.window}}])`, TotalQuery: `rate(my_metric[{{.window}}])`, }, @@ -51,29 +50,28 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { TimeWindow: 30 * 24 * time.Hour, Objective: 99.9, Labels: map[string]string{"test_label": "label_1"}, - PageAlertMeta: prometheus.AlertMeta{ + PageAlertMeta: model.PromAlertMeta{ Name: "p_alert_test_name", Labels: map[string]string{"p_alert_label": "p_label_al_1"}, Annotations: map[string]string{"p_alert_annot": "p_label_an_1"}, }, - TicketAlertMeta: prometheus.AlertMeta{ + TicketAlertMeta: model.PromAlertMeta{ Name: "t_alert_test_name", Labels: map[string]string{"t_alert_label": "t_label_al_1"}, Annotations: map[string]string{"t_alert_annot": "t_label_an_1"}, }, }, - }, - }, + }}, }, expResp: generate.Response{ PrometheusSLOs: []generate.SLOResult{ { - SLO: prometheus.SLO{ + SLO: model.PromSLO{ ID: "test-id", Name: "test-name", Service: "test-svc", - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ ErrorQuery: `rate(my_metric{error="true"}[{{.window}}])`, TotalQuery: `rate(my_metric[{{.window}}])`, }, @@ -85,12 +83,12 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { "extra_k1": "extra_v1", "extra_k2": "extra_v2", }, - PageAlertMeta: prometheus.AlertMeta{ + PageAlertMeta: model.PromAlertMeta{ Name: "p_alert_test_name", Labels: map[string]string{"p_alert_label": "p_label_al_1"}, Annotations: map[string]string{"p_alert_annot": "p_label_an_1"}, }, - TicketAlertMeta: prometheus.AlertMeta{ + TicketAlertMeta: model.PromAlertMeta{ Name: "t_alert_test_name", Labels: map[string]string{"t_alert_label": "t_label_al_1"}, Annotations: map[string]string{"t_alert_annot": "t_label_an_1"}, @@ -131,8 +129,8 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { Severity: model.TicketAlertSeverity, }, }, - SLORules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "slo:sli_error:ratio_rate5m", Expr: "(rate(my_metric{error=\"true\"}[5m]))\n/\n(rate(my_metric[5m]))\n", @@ -237,8 +235,8 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { "sloth_window": "30d", }, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ // Metadata labels. { Record: "slo:objective:ratio", @@ -334,8 +332,8 @@ slo:error_budget:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo=" "sloth_objective": "99.9", }, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "p_alert_test_name", @@ -385,7 +383,7 @@ or "title": "(ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast.", }, }, - }, + }}, }, }, }, diff --git a/internal/k8sprometheus/storage.go b/internal/k8sprometheus/storage.go index 85559ee0..c504f2e7 100644 --- a/internal/k8sprometheus/storage.go +++ b/internal/k8sprometheus/storage.go @@ -106,24 +106,24 @@ func mapModelToPrometheusOperator(ctx context.Context, kmeta K8sMeta, slos []Sto } for _, slo := range slos { - if len(slo.Rules.SLIErrorRecRules) > 0 { + if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.SLIErrorRecRules), + Rules: promRulesToKubeRules(slo.Rules.SLIErrorRecRules.Rules), }) } - if len(slo.Rules.MetadataRecRules) > 0 { + if len(slo.Rules.MetadataRecRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.MetadataRecRules), + Rules: promRulesToKubeRules(slo.Rules.MetadataRecRules.Rules), }) } - if len(slo.Rules.AlertRules) > 0 { + if len(slo.Rules.AlertRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.AlertRules), + Rules: promRulesToKubeRules(slo.Rules.AlertRules.Rules), }) } } diff --git a/internal/k8sprometheus/storage_test.go b/internal/k8sprometheus/storage_test.go index 165304e4..b019e3fb 100644 --- a/internal/k8sprometheus/storage_test.go +++ b/internal/k8sprometheus/storage_test.go @@ -18,6 +18,7 @@ import ( "github.com/slok/sloth/internal/k8sprometheus/k8sprometheusmock" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/pkg/common/model" ) func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { @@ -52,13 +53,13 @@ func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { { SLO: prometheus.SLO{ID: "test1"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, }, - }, + }}, }, }, }, @@ -101,13 +102,13 @@ spec: { SLO: prometheus.SLO{ID: "test1"}, Rules: prometheus.SLORules{ - MetadataRecRules: []rulefmt.Rule{ + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, }, - }, + }}, }, }, }, @@ -150,14 +151,14 @@ spec: { SLO: prometheus.SLO{ID: "test1"}, Rules: prometheus.SLORules{ - AlertRules: []rulefmt.Rule{ + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlert", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, Annotations: map[string]string{"test-annot": "one"}, }, - }, + }}, }, }, }, @@ -203,7 +204,7 @@ spec: { SLO: prometheus.SLO{ID: "testa"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a1", Expr: "test-expr-a1", @@ -214,8 +215,8 @@ spec: Expr: "test-expr-a2", Labels: map[string]string{"test-label": "a-2"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a3", Expr: "test-expr-a3", @@ -226,8 +227,8 @@ spec: Expr: "test-expr-a4", Labels: map[string]string{"test-label": "a-4"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertA1", Expr: "test-expr-a1", @@ -240,34 +241,34 @@ spec: Labels: map[string]string{"test-label": "a-2"}, Annotations: map[string]string{"test-annot": "a-2"}, }, - }, + }}, }, }, { SLO: prometheus.SLO{ID: "testb"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b2", Expr: "test-expr-b2", Labels: map[string]string{"test-label": "b-2"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertB1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, Annotations: map[string]string{"test-annot": "b-1"}, }, - }, + }}, }, }, }, @@ -394,9 +395,9 @@ func TestPrometheusOperatorCRDRepo(t *testing.T) { { SLO: prometheus.SLO{ID: "testa"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ {Record: "test:record-a1"}, - }, + }}, }, }, }, @@ -420,7 +421,7 @@ func TestPrometheusOperatorCRDRepo(t *testing.T) { { SLO: prometheus.SLO{ID: "testa"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a1", Expr: "test-expr-a1", @@ -431,8 +432,8 @@ func TestPrometheusOperatorCRDRepo(t *testing.T) { Expr: "test-expr-a2", Labels: map[string]string{"test-label": "a-2"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a3", Expr: "test-expr-a3", @@ -443,8 +444,8 @@ func TestPrometheusOperatorCRDRepo(t *testing.T) { Expr: "test-expr-a4", Labels: map[string]string{"test-label": "a-4"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertA1", Expr: "test-expr-a1", @@ -457,34 +458,34 @@ func TestPrometheusOperatorCRDRepo(t *testing.T) { Labels: map[string]string{"test-label": "a-2"}, Annotations: map[string]string{"test-annot": "a-2"}, }, - }, + }}, }, }, { SLO: prometheus.SLO{ID: "testb"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b2", Expr: "test-expr-b2", Labels: map[string]string{"test-label": "b-2"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertB1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, Annotations: map[string]string{"test-annot": "b-1"}, }, - }, + }}, }, }, }, diff --git a/internal/storage/io/std_prometheus.go b/internal/storage/io/std_prometheus.go index 393fa8df..9fefde55 100644 --- a/internal/storage/io/std_prometheus.go +++ b/internal/storage/io/std_prometheus.go @@ -48,24 +48,24 @@ func (r StdPrometheusGroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos [ ruleGroups := stdPromRuleGroupsYAMLv2{} for _, slo := range slos { - if len(slo.Rules.SLIErrorRecRules) > 0 { + if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), - Rules: slo.Rules.SLIErrorRecRules, + Rules: slo.Rules.SLIErrorRecRules.Rules, }) } - if len(slo.Rules.MetadataRecRules) > 0 { + if len(slo.Rules.MetadataRecRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), - Rules: slo.Rules.MetadataRecRules, + Rules: slo.Rules.MetadataRecRules.Rules, }) } - if len(slo.Rules.AlertRules) > 0 { + if len(slo.Rules.AlertRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), - Rules: slo.Rules.AlertRules, + Rules: slo.Rules.AlertRules.Rules, }) } } diff --git a/internal/storage/io/std_prometheus_test.go b/internal/storage/io/std_prometheus_test.go index 24ee9dae..daddbe4e 100644 --- a/internal/storage/io/std_prometheus_test.go +++ b/internal/storage/io/std_prometheus_test.go @@ -36,13 +36,13 @@ func TestGroupedRulesYAMLRepoStore(t *testing.T) { { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, }, - }, + }}, }, }, }, @@ -65,13 +65,13 @@ groups: { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - MetadataRecRules: []rulefmt.Rule{ + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, }, - }, + }}, }, }, }, @@ -94,14 +94,14 @@ groups: { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - AlertRules: []rulefmt.Rule{ + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlert", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, Annotations: map[string]string{"test-annot": "one"}, }, - }, + }}, }, }, }, @@ -127,7 +127,7 @@ groups: { SLO: model.PromSLO{ID: "testa"}, Rules: model.PromSLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a1", Expr: "test-expr-a1", @@ -138,8 +138,8 @@ groups: Expr: "test-expr-a2", Labels: map[string]string{"test-label": "a-2"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a3", Expr: "test-expr-a3", @@ -150,8 +150,8 @@ groups: Expr: "test-expr-a4", Labels: map[string]string{"test-label": "a-4"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertA1", Expr: "test-expr-a1", @@ -164,34 +164,34 @@ groups: Labels: map[string]string{"test-label": "a-2"}, Annotations: map[string]string{"test-annot": "a-2"}, }, - }, + }}, }, }, { SLO: model.PromSLO{ID: "testb"}, Rules: model.PromSLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b2", Expr: "test-expr-b2", Labels: map[string]string{"test-label": "b-2"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertB1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, Annotations: map[string]string{"test-annot": "b-1"}, }, - }, + }}, }, }, }, diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index 5f3bc1e5..af76d032 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -282,7 +282,12 @@ func validateSLOGroup(sl validator.StructLevel) { // PromSLORules are the prometheus rules required by an SLO. type PromSLORules struct { - SLIErrorRecRules []rulefmt.Rule - MetadataRecRules []rulefmt.Rule - AlertRules []rulefmt.Rule + SLIErrorRecRules PromRuleGroup + MetadataRecRules PromRuleGroup + AlertRules PromRuleGroup +} + +// PromRuleGroup are regular prometheus group of rules. +type PromRuleGroup struct { + Rules []rulefmt.Rule } From 8d5c73dbee483a4650f1896cc2d1eb8b23b5f995 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Mon, 31 Mar 2025 19:22:11 +0200 Subject: [PATCH 031/173] Move apiserver storage to storage package Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/k8scontroller.go | 7 +- internal/app/generate/prometheus.go | 14 +-- internal/app/generate/prometheus_test.go | 38 ++++---- internal/k8sprometheus/storage.go | 12 +-- internal/k8sprometheus/storage_test.go | 67 +++++++------- internal/storage/io/std_prometheus.go | 12 +-- internal/storage/io/std_prometheus_test.go | 36 ++++---- .../kubernetes.go => storage/k8s/k8s.go} | 90 +++++++++---------- pkg/common/model/slo_prometheus.go | 11 ++- 9 files changed, 147 insertions(+), 140 deletions(-) rename internal/{k8sprometheus/kubernetes.go => storage/k8s/k8s.go} (57%) diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index bb874654..c4072998 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -37,6 +37,7 @@ import ( "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/prometheus" storageio "github.com/slok/sloth/internal/storage/io" + storagek8s "github.com/slok/sloth/internal/storage/k8s" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientset "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" ) @@ -382,7 +383,7 @@ func (k kubeControllerCommand) newKubernetesService(ctx context.Context, config // Fake mode. if k.runMode == controllerModeFake { - return k8sprometheus.NewKubernetesServiceFake(config.Logger), nil + return storagek8s.NewFakeApiserverRepository(config.Logger), nil } // Load Kubernetes clients. @@ -402,12 +403,12 @@ func (k kubeControllerCommand) newKubernetesService(ctx context.Context, config } // Create Kubernetes service. - ksvc := k8sprometheus.NewKubernetesService(kubeSlothcli, kubeMonitoringCli, config.Logger) + ksvc := storagek8s.NewApiserverRepository(kubeSlothcli, kubeMonitoringCli, config.Logger) // Dry run mode. if k.runMode == controllerModeDryRun { config.Logger.Warningf("Kubernetes in dry run mode") - return k8sprometheus.NewKubernetesServiceDryRun(ksvc, config.Logger), nil + return storagek8s.NewDryRunApiserverRepository(ksvc, config.Logger), nil } // Default mode. diff --git a/internal/app/generate/prometheus.go b/internal/app/generate/prometheus.go index fd0e2976..d8a7b588 100644 --- a/internal/app/generate/prometheus.go +++ b/internal/app/generate/prometheus.go @@ -98,13 +98,13 @@ type Request struct { // ExtraLabels are the extra labels added to the SLOs on execution time. ExtraLabels map[string]string // SLOGroup are the SLOs group that will be used to generate the SLO results and Prom rules. - SLOGroup prometheus.SLOGroup + SLOGroup model.PromSLOGroup } type SLOResult struct { - SLO prometheus.SLO + SLO model.PromSLO Alerts model.MWMBAlertGroup - SLORules prometheus.SLORules + SLORules model.PromSLORules } type Response struct { @@ -176,10 +176,10 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, slo prometheu return &SLOResult{ SLO: slo, Alerts: *as, - SLORules: prometheus.SLORules{ - SLIErrorRecRules: sliRecordingRules, - MetadataRecRules: metaRecordingRules, - AlertRules: alertRules, + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: sliRecordingRules}, + MetadataRecRules: model.PromRuleGroup{Rules: metaRecordingRules}, + AlertRules: model.PromRuleGroup{Rules: alertRules}, }, }, nil } diff --git a/internal/app/generate/prometheus_test.go b/internal/app/generate/prometheus_test.go index 0d72e66e..c9db36c1 100644 --- a/internal/app/generate/prometheus_test.go +++ b/internal/app/generate/prometheus_test.go @@ -11,7 +11,6 @@ import ( "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/app/generate" - "github.com/slok/sloth/internal/prometheus" "github.com/slok/sloth/pkg/common/model" ) @@ -37,13 +36,13 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { Mode: model.ModeTest, Spec: "test-spec", }, - SLOGroup: prometheus.SLOGroup{SLOs: []prometheus.SLO{ + SLOGroup: model.PromSLOGroup{SLOs: []model.PromSLO{ { ID: "test-id", Name: "test-name", Service: "test-svc", - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ ErrorQuery: `rate(my_metric{error="true"}[{{.window}}])`, TotalQuery: `rate(my_metric[{{.window}}])`, }, @@ -51,29 +50,28 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { TimeWindow: 30 * 24 * time.Hour, Objective: 99.9, Labels: map[string]string{"test_label": "label_1"}, - PageAlertMeta: prometheus.AlertMeta{ + PageAlertMeta: model.PromAlertMeta{ Name: "p_alert_test_name", Labels: map[string]string{"p_alert_label": "p_label_al_1"}, Annotations: map[string]string{"p_alert_annot": "p_label_an_1"}, }, - TicketAlertMeta: prometheus.AlertMeta{ + TicketAlertMeta: model.PromAlertMeta{ Name: "t_alert_test_name", Labels: map[string]string{"t_alert_label": "t_label_al_1"}, Annotations: map[string]string{"t_alert_annot": "t_label_an_1"}, }, }, - }, - }, + }}, }, expResp: generate.Response{ PrometheusSLOs: []generate.SLOResult{ { - SLO: prometheus.SLO{ + SLO: model.PromSLO{ ID: "test-id", Name: "test-name", Service: "test-svc", - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ ErrorQuery: `rate(my_metric{error="true"}[{{.window}}])`, TotalQuery: `rate(my_metric[{{.window}}])`, }, @@ -85,12 +83,12 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { "extra_k1": "extra_v1", "extra_k2": "extra_v2", }, - PageAlertMeta: prometheus.AlertMeta{ + PageAlertMeta: model.PromAlertMeta{ Name: "p_alert_test_name", Labels: map[string]string{"p_alert_label": "p_label_al_1"}, Annotations: map[string]string{"p_alert_annot": "p_label_an_1"}, }, - TicketAlertMeta: prometheus.AlertMeta{ + TicketAlertMeta: model.PromAlertMeta{ Name: "t_alert_test_name", Labels: map[string]string{"t_alert_label": "t_label_al_1"}, Annotations: map[string]string{"t_alert_annot": "t_label_an_1"}, @@ -131,8 +129,8 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { Severity: model.TicketAlertSeverity, }, }, - SLORules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "slo:sli_error:ratio_rate5m", Expr: "(rate(my_metric{error=\"true\"}[5m]))\n/\n(rate(my_metric[5m]))\n", @@ -237,8 +235,8 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { "sloth_window": "30d", }, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ // Metadata labels. { Record: "slo:objective:ratio", @@ -334,8 +332,8 @@ slo:error_budget:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo=" "sloth_objective": "99.9", }, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "p_alert_test_name", @@ -385,7 +383,7 @@ or "title": "(ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast.", }, }, - }, + }}, }, }, }, diff --git a/internal/k8sprometheus/storage.go b/internal/k8sprometheus/storage.go index 85559ee0..c504f2e7 100644 --- a/internal/k8sprometheus/storage.go +++ b/internal/k8sprometheus/storage.go @@ -106,24 +106,24 @@ func mapModelToPrometheusOperator(ctx context.Context, kmeta K8sMeta, slos []Sto } for _, slo := range slos { - if len(slo.Rules.SLIErrorRecRules) > 0 { + if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.SLIErrorRecRules), + Rules: promRulesToKubeRules(slo.Rules.SLIErrorRecRules.Rules), }) } - if len(slo.Rules.MetadataRecRules) > 0 { + if len(slo.Rules.MetadataRecRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.MetadataRecRules), + Rules: promRulesToKubeRules(slo.Rules.MetadataRecRules.Rules), }) } - if len(slo.Rules.AlertRules) > 0 { + if len(slo.Rules.AlertRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.AlertRules), + Rules: promRulesToKubeRules(slo.Rules.AlertRules.Rules), }) } } diff --git a/internal/k8sprometheus/storage_test.go b/internal/k8sprometheus/storage_test.go index 165304e4..989dff02 100644 --- a/internal/k8sprometheus/storage_test.go +++ b/internal/k8sprometheus/storage_test.go @@ -1,5 +1,6 @@ package k8sprometheus_test +/* import ( "bytes" "context" @@ -18,6 +19,7 @@ import ( "github.com/slok/sloth/internal/k8sprometheus/k8sprometheusmock" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/pkg/common/model" ) func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { @@ -52,13 +54,13 @@ func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { { SLO: prometheus.SLO{ID: "test1"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, }, - }, + }}, }, }, }, @@ -101,13 +103,13 @@ spec: { SLO: prometheus.SLO{ID: "test1"}, Rules: prometheus.SLORules{ - MetadataRecRules: []rulefmt.Rule{ + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, }, - }, + }}, }, }, }, @@ -150,14 +152,14 @@ spec: { SLO: prometheus.SLO{ID: "test1"}, Rules: prometheus.SLORules{ - AlertRules: []rulefmt.Rule{ + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlert", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, Annotations: map[string]string{"test-annot": "one"}, }, - }, + }}, }, }, }, @@ -203,7 +205,7 @@ spec: { SLO: prometheus.SLO{ID: "testa"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a1", Expr: "test-expr-a1", @@ -214,8 +216,8 @@ spec: Expr: "test-expr-a2", Labels: map[string]string{"test-label": "a-2"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a3", Expr: "test-expr-a3", @@ -226,8 +228,8 @@ spec: Expr: "test-expr-a4", Labels: map[string]string{"test-label": "a-4"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertA1", Expr: "test-expr-a1", @@ -240,34 +242,34 @@ spec: Labels: map[string]string{"test-label": "a-2"}, Annotations: map[string]string{"test-annot": "a-2"}, }, - }, + }}, }, }, { SLO: prometheus.SLO{ID: "testb"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b2", Expr: "test-expr-b2", Labels: map[string]string{"test-label": "b-2"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertB1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, Annotations: map[string]string{"test-annot": "b-1"}, }, - }, + }}, }, }, }, @@ -394,9 +396,9 @@ func TestPrometheusOperatorCRDRepo(t *testing.T) { { SLO: prometheus.SLO{ID: "testa"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ {Record: "test:record-a1"}, - }, + }}, }, }, }, @@ -420,7 +422,7 @@ func TestPrometheusOperatorCRDRepo(t *testing.T) { { SLO: prometheus.SLO{ID: "testa"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a1", Expr: "test-expr-a1", @@ -431,8 +433,8 @@ func TestPrometheusOperatorCRDRepo(t *testing.T) { Expr: "test-expr-a2", Labels: map[string]string{"test-label": "a-2"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a3", Expr: "test-expr-a3", @@ -443,8 +445,8 @@ func TestPrometheusOperatorCRDRepo(t *testing.T) { Expr: "test-expr-a4", Labels: map[string]string{"test-label": "a-4"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertA1", Expr: "test-expr-a1", @@ -457,34 +459,34 @@ func TestPrometheusOperatorCRDRepo(t *testing.T) { Labels: map[string]string{"test-label": "a-2"}, Annotations: map[string]string{"test-annot": "a-2"}, }, - }, + }}, }, }, { SLO: prometheus.SLO{ID: "testb"}, Rules: prometheus.SLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b2", Expr: "test-expr-b2", Labels: map[string]string{"test-label": "b-2"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertB1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, Annotations: map[string]string{"test-annot": "b-1"}, }, - }, + }}, }, }, }, @@ -620,3 +622,4 @@ func TestPrometheusOperatorCRDRepo(t *testing.T) { }) } } +*/ diff --git a/internal/storage/io/std_prometheus.go b/internal/storage/io/std_prometheus.go index 393fa8df..9fefde55 100644 --- a/internal/storage/io/std_prometheus.go +++ b/internal/storage/io/std_prometheus.go @@ -48,24 +48,24 @@ func (r StdPrometheusGroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos [ ruleGroups := stdPromRuleGroupsYAMLv2{} for _, slo := range slos { - if len(slo.Rules.SLIErrorRecRules) > 0 { + if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), - Rules: slo.Rules.SLIErrorRecRules, + Rules: slo.Rules.SLIErrorRecRules.Rules, }) } - if len(slo.Rules.MetadataRecRules) > 0 { + if len(slo.Rules.MetadataRecRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), - Rules: slo.Rules.MetadataRecRules, + Rules: slo.Rules.MetadataRecRules.Rules, }) } - if len(slo.Rules.AlertRules) > 0 { + if len(slo.Rules.AlertRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), - Rules: slo.Rules.AlertRules, + Rules: slo.Rules.AlertRules.Rules, }) } } diff --git a/internal/storage/io/std_prometheus_test.go b/internal/storage/io/std_prometheus_test.go index 24ee9dae..daddbe4e 100644 --- a/internal/storage/io/std_prometheus_test.go +++ b/internal/storage/io/std_prometheus_test.go @@ -36,13 +36,13 @@ func TestGroupedRulesYAMLRepoStore(t *testing.T) { { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, }, - }, + }}, }, }, }, @@ -65,13 +65,13 @@ groups: { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - MetadataRecRules: []rulefmt.Rule{ + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, }, - }, + }}, }, }, }, @@ -94,14 +94,14 @@ groups: { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - AlertRules: []rulefmt.Rule{ + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlert", Expr: "test-expr", Labels: map[string]string{"test-label": "one"}, Annotations: map[string]string{"test-annot": "one"}, }, - }, + }}, }, }, }, @@ -127,7 +127,7 @@ groups: { SLO: model.PromSLO{ID: "testa"}, Rules: model.PromSLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a1", Expr: "test-expr-a1", @@ -138,8 +138,8 @@ groups: Expr: "test-expr-a2", Labels: map[string]string{"test-label": "a-2"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a3", Expr: "test-expr-a3", @@ -150,8 +150,8 @@ groups: Expr: "test-expr-a4", Labels: map[string]string{"test-label": "a-4"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertA1", Expr: "test-expr-a1", @@ -164,34 +164,34 @@ groups: Labels: map[string]string{"test-label": "a-2"}, Annotations: map[string]string{"test-annot": "a-2"}, }, - }, + }}, }, }, { SLO: model.PromSLO{ID: "testb"}, Rules: model.PromSLORules{ - SLIErrorRecRules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, }, - }, - MetadataRecRules: []rulefmt.Rule{ + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b2", Expr: "test-expr-b2", Labels: map[string]string{"test-label": "b-2"}, }, - }, - AlertRules: []rulefmt.Rule{ + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlertB1", Expr: "test-expr-b1", Labels: map[string]string{"test-label": "b-1"}, Annotations: map[string]string{"test-annot": "b-1"}, }, - }, + }}, }, }, }, diff --git a/internal/k8sprometheus/kubernetes.go b/internal/storage/k8s/k8s.go similarity index 57% rename from internal/k8sprometheus/kubernetes.go rename to internal/storage/k8s/k8s.go index 75a0fb83..cbbb5f83 100644 --- a/internal/k8sprometheus/kubernetes.go +++ b/internal/storage/k8s/k8s.go @@ -1,4 +1,4 @@ -package k8sprometheus +package k8s import ( "context" @@ -18,38 +18,38 @@ import ( slothclientsetfake "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/fake" ) -type KubernetesService struct { +type ApiserverRepository struct { slothCli slothclientset.Interface monitoringCli monitoringclientset.Interface logger log.Logger } -// NewKubernetesService returns a new Kubernetes Service. -func NewKubernetesService(slothCli slothclientset.Interface, monitoringCli monitoringclientset.Interface, logger log.Logger) KubernetesService { - return KubernetesService{ +// NewApiserverRepository returns a new Kubernetes Apiserver storage. +func NewApiserverRepository(slothCli slothclientset.Interface, monitoringCli monitoringclientset.Interface, logger log.Logger) ApiserverRepository { + return ApiserverRepository{ slothCli: slothCli, monitoringCli: monitoringCli, - logger: logger.WithValues(log.Kv{"service": "k8sprometheus.Service"}), + logger: logger.WithValues(log.Kv{"service": "storage.k8s."}), } } -func (k KubernetesService) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { - return k.slothCli.SlothV1().PrometheusServiceLevels(ns).List(ctx, opts) +func (r ApiserverRepository) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { + return r.slothCli.SlothV1().PrometheusServiceLevels(ns).List(ctx, opts) } -func (k KubernetesService) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) { - return k.slothCli.SlothV1().PrometheusServiceLevels(ns).Watch(ctx, opts) +func (r ApiserverRepository) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) { + return r.slothCli.SlothV1().PrometheusServiceLevels(ns).Watch(ctx, opts) } -func (k KubernetesService) EnsurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error { - logger := k.logger.WithCtxValues(ctx) +func (r ApiserverRepository) EnsurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error { + logger := r.logger.WithCtxValues(ctx) pr = pr.DeepCopy() - stored, err := k.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Get(ctx, pr.Name, metav1.GetOptions{}) + stored, err := r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Get(ctx, pr.Name, metav1.GetOptions{}) if err != nil { if !kubeerrors.IsNotFound(err) { return err } - _, err = k.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Create(ctx, pr, metav1.CreateOptions{}) + _, err = r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Create(ctx, pr, metav1.CreateOptions{}) if err != nil { return err } @@ -60,7 +60,7 @@ func (k KubernetesService) EnsurePrometheusRule(ctx context.Context, pr *monitor // Force overwrite. pr.ObjectMeta.ResourceVersion = stored.ResourceVersion - _, err = k.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Update(ctx, pr, metav1.UpdateOptions{}) + _, err = r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Update(ctx, pr, metav1.UpdateOptions{}) if err != nil { return err } @@ -73,7 +73,7 @@ func (k KubernetesService) EnsurePrometheusRule(ctx context.Context, pr *monitor // an status will trigger a watch update event on a controller. // In case of no error we will update "last correct Prometheus operation rules generated" TS so we can be in // a infinite loop of handling, the handler should break this loop somehow (e.g: if ok and last generated < 5m, ignore). -func (k KubernetesService) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error { +func (r ApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error { slo = slo.DeepCopy() slo.Status.PromOpRulesGenerated = false @@ -87,70 +87,70 @@ func (k KubernetesService) EnsurePrometheusServiceLevelStatus(ctx context.Contex slo.Status.LastPromOpRulesSuccessfulGenerated = &metav1.Time{Time: time.Now().UTC()} } - _, err = k.slothCli.SlothV1().PrometheusServiceLevels(slo.Namespace).UpdateStatus(ctx, slo, metav1.UpdateOptions{}) + _, err = r.slothCli.SlothV1().PrometheusServiceLevels(slo.Namespace).UpdateStatus(ctx, slo, metav1.UpdateOptions{}) return err } -type DryRunKubernetesService struct { - svc KubernetesService +type DryRunApiserverRepository struct { + svc ApiserverRepository logger log.Logger } -// NewKubernetesServiceDryRun returns a new Kubernetes Service that will dry-run that will only do real ReadOnly operations. -func NewKubernetesServiceDryRun(svc KubernetesService, logger log.Logger) DryRunKubernetesService { - return DryRunKubernetesService{ +// NewDryRunApiserverRepository returns a new Kubernetes Service that will dry-run that will only do real ReadOnly operations. +func NewDryRunApiserverRepository(svc ApiserverRepository, logger log.Logger) DryRunApiserverRepository { + return DryRunApiserverRepository{ svc: svc, - logger: logger.WithValues(log.Kv{"service": "k8sprometheus.DryRunService"}), + logger: logger.WithValues(log.Kv{"service": "storage.k8s.DryRunApiserverRepository"}), } } -func (d DryRunKubernetesService) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { - return d.svc.ListPrometheusServiceLevels(ctx, ns, opts) +func (r DryRunApiserverRepository) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { + return r.svc.ListPrometheusServiceLevels(ctx, ns, opts) } -func (d DryRunKubernetesService) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) { - return d.svc.WatchPrometheusServiceLevels(ctx, ns, opts) +func (r DryRunApiserverRepository) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) { + return r.svc.WatchPrometheusServiceLevels(ctx, ns, opts) } -func (d DryRunKubernetesService) EnsurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error { - d.logger.Infof("Dry run EnsurePrometheusRule") +func (r DryRunApiserverRepository) EnsurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error { + r.logger.Infof("Dry run EnsurePrometheusRule") return nil } -func (d DryRunKubernetesService) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error { - d.logger.Infof("Dry run EnsurePrometheusServiceLevelStatus") +func (r DryRunApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error { + r.logger.Infof("Dry run EnsurePrometheusServiceLevelStatus") return nil } -type FakeKubernetesService struct { - ksvc KubernetesService +type FakeApiserverRepository struct { + ksvc ApiserverRepository } -// NewKubernetesServiceFake returns a new Kubernetes Service that will fake Kubernetes operations +// NewFakeApiserverRepository returns a new Kubernetes Service that will fake Kubernetes operations // using fake clients. -func NewKubernetesServiceFake(logger log.Logger) FakeKubernetesService { - return FakeKubernetesService{ - ksvc: NewKubernetesService( +func NewFakeApiserverRepository(logger log.Logger) FakeApiserverRepository { + return FakeApiserverRepository{ + ksvc: NewApiserverRepository( slothclientsetfake.NewSimpleClientset(prometheusServiceLevelFakes...), monitoringclientsetfake.NewSimpleClientset(), logger), } } -func (f FakeKubernetesService) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { - return f.ksvc.ListPrometheusServiceLevels(ctx, ns, opts) +func (r FakeApiserverRepository) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { + return r.ksvc.ListPrometheusServiceLevels(ctx, ns, opts) } -func (f FakeKubernetesService) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) { - return f.ksvc.WatchPrometheusServiceLevels(ctx, ns, opts) +func (r FakeApiserverRepository) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) { + return r.ksvc.WatchPrometheusServiceLevels(ctx, ns, opts) } -func (f FakeKubernetesService) EnsurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error { - return f.ksvc.EnsurePrometheusRule(ctx, pr) +func (r FakeApiserverRepository) EnsurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error { + return r.ksvc.EnsurePrometheusRule(ctx, pr) } -func (f FakeKubernetesService) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error { - return f.ksvc.EnsurePrometheusServiceLevelStatus(ctx, slo, err) +func (r FakeApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error { + return r.ksvc.EnsurePrometheusServiceLevelStatus(ctx, slo, err) } var prometheusServiceLevelFakes = []runtime.Object{ diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index 5f3bc1e5..af76d032 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -282,7 +282,12 @@ func validateSLOGroup(sl validator.StructLevel) { // PromSLORules are the prometheus rules required by an SLO. type PromSLORules struct { - SLIErrorRecRules []rulefmt.Rule - MetadataRecRules []rulefmt.Rule - AlertRules []rulefmt.Rule + SLIErrorRecRules PromRuleGroup + MetadataRecRules PromRuleGroup + AlertRules PromRuleGroup +} + +// PromRuleGroup are regular prometheus group of rules. +type PromRuleGroup struct { + Rules []rulefmt.Rule } From 84258b60dbea7689d6fa61580e7731befc1c7287 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Tue, 1 Apr 2025 12:28:28 +0200 Subject: [PATCH 032/173] Move kubernetes logic to their own packages in storage Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 10 +- cmd/sloth/commands/k8scontroller.go | 21 +- internal/app/kubecontroller/handler.go | 10 +- .../prometheus_rules_ensurer.go | 48 -- internal/k8sprometheus/storage.go | 219 ------ internal/k8sprometheus/storage_test.go | 625 ------------------ internal/kubernetes/modelmap/slo.go | 99 +++ internal/storage/io/prometheus_operator.go | 52 ++ .../storage/io/prometheus_operator_test.go | 361 ++++++++++ internal/storage/k8s/dry_run.go | 43 ++ internal/storage/k8s/fake.go | 102 +++ internal/storage/k8s/k8s.go | 184 ++---- internal/storage/k8s/k8s_test.go | 262 ++++++++ internal/storage/storage.go | 21 + pkg/common/errors/errors.go | 9 + 15 files changed, 1015 insertions(+), 1051 deletions(-) delete mode 100644 internal/k8sprometheus/k8sprometheusmock/prometheus_rules_ensurer.go delete mode 100644 internal/k8sprometheus/storage.go delete mode 100644 internal/k8sprometheus/storage_test.go create mode 100644 internal/kubernetes/modelmap/slo.go create mode 100644 internal/storage/io/prometheus_operator.go create mode 100644 internal/storage/io/prometheus_operator_test.go create mode 100644 internal/storage/k8s/dry_run.go create mode 100644 internal/storage/k8s/fake.go create mode 100644 internal/storage/k8s/k8s_test.go create mode 100644 internal/storage/storage.go create mode 100644 pkg/common/errors/errors.go diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 123609d5..a1b75c4a 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -19,9 +19,9 @@ import ( "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/info" - "github.com/slok/sloth/internal/k8sprometheus" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/internal/storage" storageio "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" kubernetesv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" @@ -353,16 +353,16 @@ func (g generator) GenerateKubernetes(ctx context.Context, sloGroup model.PromSL return err } - repo := k8sprometheus.NewIOWriterPrometheusOperatorYAMLRepo(out, g.logger) - storageSLOs := make([]k8sprometheus.StorageSLO, 0, len(result.PrometheusSLOs)) + repo := storageio.NewIOWriterPrometheusOperatorYAMLRepo(out, g.logger) + storageSLOs := make([]storage.SLORulesResult, 0, len(result.PrometheusSLOs)) for _, s := range result.PrometheusSLOs { - storageSLOs = append(storageSLOs, k8sprometheus.StorageSLO{ + storageSLOs = append(storageSLOs, storage.SLORulesResult{ SLO: s.SLO, Rules: s.SLORules, }) } - kmeta := k8sprometheus.K8sMeta{ + kmeta := storage.K8sMeta{ Kind: "PrometheusServiceLevel", APIVersion: "sloth.slok.dev/v1", UID: string(sloGroup.OriginalSource.K8sSlothV1.UID), diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index c4072998..305a4be6 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -14,7 +14,6 @@ import ( "github.com/alecthomas/kingpin/v2" "github.com/oklog/run" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" monitoringclientset "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" "github.com/prometheus/client_golang/prometheus/promhttp" prometheusmodel "github.com/prometheus/common/model" @@ -33,9 +32,9 @@ import ( "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/app/kubecontroller" - "github.com/slok/sloth/internal/k8sprometheus" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/internal/storage" storageio "github.com/slok/sloth/internal/storage/io" storagek8s "github.com/slok/sloth/internal/storage/k8s" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" @@ -139,14 +138,14 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error } // Kubernetes services. - ksvc, err := k.newKubernetesService(ctx, config) + kuberepo, err := k.newKubernetesService(ctx, config) if err != nil { return fmt.Errorf("could not create Kubernetes service: %w", err) } // Check we can get Sloth CRs without problem before starting everything. This is a hard // dependency, if we can't, we must fail. - _, err = ksvc.ListPrometheusServiceLevels(ctx, k.namespace, metav1.ListOptions{}) + _, err = kuberepo.ListPrometheusServiceLevels(ctx, k.namespace, metav1.ListOptions{}) if err != nil { return fmt.Errorf("check for PrometheusServiceLevel CRD failed: could not list: %w", err) } @@ -322,8 +321,8 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error config := kubecontroller.HandlerConfig{ Generator: generator, SpecLoader: storageio.NewK8sSlothPrometheusCRSpecLoader(pluginRepo, sloPeriod), - Repository: k8sprometheus.NewPrometheusOperatorCRDRepo(ksvc, logger), - KubeStatusStorer: ksvc, + Repository: kuberepo, + KubeStatusStorer: kuberepo, ExtraLabels: k.extraLabels, Logger: logger, } @@ -338,7 +337,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error return fmt.Errorf("invalid label selector %q: %w", k.labelSelector, err) } - ret := kubecontroller.NewPrometheusServiceLevelsRetriver(k.namespace, lSelector, ksvc) + ret := kubecontroller.NewPrometheusServiceLevelsRetriver(k.namespace, lSelector, kuberepo) ctrl, err := koopercontroller.New(&koopercontroller.Config{ Handler: handler, @@ -374,8 +373,8 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error type kubernetesService interface { ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) - EnsurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error + StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error } func (k kubeControllerCommand) newKubernetesService(ctx context.Context, config RootConfig) (kubernetesService, error) { @@ -403,16 +402,16 @@ func (k kubeControllerCommand) newKubernetesService(ctx context.Context, config } // Create Kubernetes service. - ksvc := storagek8s.NewApiserverRepository(kubeSlothcli, kubeMonitoringCli, config.Logger) + kuberepo := storagek8s.NewApiserverRepository(kubeSlothcli, kubeMonitoringCli, config.Logger) // Dry run mode. if k.runMode == controllerModeDryRun { config.Logger.Warningf("Kubernetes in dry run mode") - return storagek8s.NewDryRunApiserverRepository(ksvc, config.Logger), nil + return storagek8s.NewDryRunApiserverRepository(kuberepo, config.Logger), nil } // Default mode. - return ksvc, nil + return kuberepo, nil } // loadKubernetesConfig loads kubernetes configuration based on flags. diff --git a/internal/app/kubecontroller/handler.go b/internal/app/kubecontroller/handler.go index fda903ae..bccdae03 100644 --- a/internal/app/kubecontroller/handler.go +++ b/internal/app/kubecontroller/handler.go @@ -10,8 +10,8 @@ import ( "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/info" - "github.com/slok/sloth/internal/k8sprometheus" "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/internal/storage" "github.com/slok/sloth/pkg/common/model" commonmodel "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" @@ -29,7 +29,7 @@ type Generator interface { // Repository knows how to store generated SLO Prometheus rules. type Repository interface { - StoreSLOs(ctx context.Context, kmeta k8sprometheus.K8sMeta, slos []k8sprometheus.StorageSLO) error + StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error } // KubeStatusStorer knows how to set the status of Prometheus service levels Kubernetes CRD. @@ -162,15 +162,15 @@ func (h handler) handlePrometheusServiceLevelV1(ctx context.Context, psl *slothv } // Store on k8s as Prometheus operator Rules. - storageSLOs := make([]k8sprometheus.StorageSLO, 0, len(resp.PrometheusSLOs)) + storageSLOs := make([]storage.SLORulesResult, 0, len(resp.PrometheusSLOs)) for _, s := range resp.PrometheusSLOs { - storageSLOs = append(storageSLOs, k8sprometheus.StorageSLO{ + storageSLOs = append(storageSLOs, storage.SLORulesResult{ SLO: s.SLO, Rules: s.SLORules, }) } - kmeta := k8sprometheus.K8sMeta{ + kmeta := storage.K8sMeta{ Kind: "PrometheusServiceLevel", APIVersion: "sloth.slok.dev/v1", UID: string(model.OriginalSource.K8sSlothV1.UID), diff --git a/internal/k8sprometheus/k8sprometheusmock/prometheus_rules_ensurer.go b/internal/k8sprometheus/k8sprometheusmock/prometheus_rules_ensurer.go deleted file mode 100644 index 4549b925..00000000 --- a/internal/k8sprometheus/k8sprometheusmock/prometheus_rules_ensurer.go +++ /dev/null @@ -1,48 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package k8sprometheusmock - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - - v1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" -) - -// PrometheusRulesEnsurer is an autogenerated mock type for the PrometheusRulesEnsurer type -type PrometheusRulesEnsurer struct { - mock.Mock -} - -// EnsurePrometheusRule provides a mock function with given fields: ctx, pr -func (_m *PrometheusRulesEnsurer) EnsurePrometheusRule(ctx context.Context, pr *v1.PrometheusRule) error { - ret := _m.Called(ctx, pr) - - if len(ret) == 0 { - panic("no return value specified for EnsurePrometheusRule") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *v1.PrometheusRule) error); ok { - r0 = rf(ctx, pr) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// NewPrometheusRulesEnsurer creates a new instance of PrometheusRulesEnsurer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewPrometheusRulesEnsurer(t interface { - mock.TestingT - Cleanup(func()) -}) *PrometheusRulesEnsurer { - mock := &PrometheusRulesEnsurer{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/k8sprometheus/storage.go b/internal/k8sprometheus/storage.go deleted file mode 100644 index c504f2e7..00000000 --- a/internal/k8sprometheus/storage.go +++ /dev/null @@ -1,219 +0,0 @@ -package k8sprometheus - -import ( - "bytes" - "context" - "fmt" - "io" - - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - "github.com/prometheus/prometheus/model/rulefmt" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer/json" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" - - "github.com/slok/sloth/internal/info" - "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/prometheus" -) - -// K8sMeta is the Kubernetes metadata simplified. -type K8sMeta struct { - Kind string `validate:"required"` - APIVersion string `validate:"required"` - Name string `validate:"required"` - UID string - Namespace string - Annotations map[string]string - Labels map[string]string -} - -var ( - // ErrNoSLORules will be used when there are no rules to store. The upper layer - // could ignore or handle the error in cases where there wasn't an output. - ErrNoSLORules = fmt.Errorf("0 SLO Prometheus rules generated") -) - -func NewIOWriterPrometheusOperatorYAMLRepo(writer io.Writer, logger log.Logger) IOWriterPrometheusOperatorYAMLRepo { - return IOWriterPrometheusOperatorYAMLRepo{ - writer: writer, - encoder: json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil), - logger: logger.WithValues(log.Kv{"svc": "storage.IOWriter", "format": "k8s-prometheus-operator"}), - } -} - -// IOWriterPrometheusOperatorYAMLRepo knows to store all the SLO rules (recordings and alerts) -// grouped in an IOWriter in Kubernetes prometheus operator YAML format. -type IOWriterPrometheusOperatorYAMLRepo struct { - writer io.Writer - encoder runtime.Encoder - logger log.Logger -} - -type StorageSLO struct { - SLO prometheus.SLO - Rules prometheus.SLORules -} - -func (i IOWriterPrometheusOperatorYAMLRepo) StoreSLOs(ctx context.Context, kmeta K8sMeta, slos []StorageSLO) error { - rule, err := mapModelToPrometheusOperator(ctx, kmeta, slos) - if err != nil { - return fmt.Errorf("could not map model to Prometheus operator CR: %w", err) - } - - var b bytes.Buffer - err = i.encoder.Encode(rule, &b) - if err != nil { - return fmt.Errorf("could encode prometheus operator object: %w", err) - } - - rulesYaml := writeTopDisclaimer(b.Bytes()) - _, err = i.writer.Write(rulesYaml) - if err != nil { - return fmt.Errorf("could not write top disclaimer: %w", err) - } - - return nil -} - -func mapModelToPrometheusOperator(ctx context.Context, kmeta K8sMeta, slos []StorageSLO) (*monitoringv1.PrometheusRule, error) { - // Add extra labels. - labels := map[string]string{ - "app.kubernetes.io/component": "SLO", - "app.kubernetes.io/managed-by": "sloth", - } - for k, v := range kmeta.Labels { - labels[k] = v - } - - rule := &monitoringv1.PrometheusRule{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "monitoring.coreos.com/v1", - Kind: "PrometheusRule", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: kmeta.Name, - Namespace: kmeta.Namespace, - Labels: labels, - Annotations: kmeta.Annotations, - }, - } - - if len(slos) == 0 { - return nil, fmt.Errorf("slo rules required") - } - - for _, slo := range slos { - if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { - rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.SLIErrorRecRules.Rules), - }) - } - - if len(slo.Rules.MetadataRecRules.Rules) > 0 { - rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.MetadataRecRules.Rules), - }) - } - - if len(slo.Rules.AlertRules.Rules) > 0 { - rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.AlertRules.Rules), - }) - } - } - - // If we don't have anything to store, error so we can increase the reliability - // because maybe this was due to an unintended error (typos, misconfig, too many disable...). - if len(rule.Spec.Groups) == 0 { - return nil, ErrNoSLORules - } - - return rule, nil -} - -func promRulesToKubeRules(rules []rulefmt.Rule) []monitoringv1.Rule { - res := make([]monitoringv1.Rule, 0, len(rules)) - for _, r := range rules { - forS := "" - if r.For != 0 { - forS = r.For.String() - } - - var dur *monitoringv1.Duration - if forS != "" { - d := monitoringv1.Duration(forS) - dur = &d - } - - res = append(res, monitoringv1.Rule{ - Record: r.Record, - Alert: r.Alert, - Expr: intstr.FromString(r.Expr), - For: dur, - Labels: r.Labels, - Annotations: r.Annotations, - }) - } - return res -} - -func writeTopDisclaimer(bs []byte) []byte { - return append([]byte(disclaimer), bs...) -} - -var disclaimer = fmt.Sprintf(` ---- -# Code generated by Sloth (%s): https://github.com/slok/sloth. -# DO NOT EDIT. - -`, info.Version) - -func NewPrometheusOperatorCRDRepo(ensurer PrometheusRulesEnsurer, logger log.Logger) PrometheusOperatorCRDRepo { - return PrometheusOperatorCRDRepo{ - ensurer: ensurer, - logger: logger.WithValues(log.Kv{"svc": "storage.PrometheusOperatorCRDAPIServer", "format": "k8s-prometheus-operator"}), - } -} - -// PrometheusOperatorCRDRepo knows to store all the SLO rules (recordings and alerts) -// grouped as a Kubernetes prometheus operator CR using Kubernetes API server. -type PrometheusOperatorCRDRepo struct { - logger log.Logger - ensurer PrometheusRulesEnsurer -} - -type PrometheusRulesEnsurer interface { - EnsurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error -} - -//go:generate mockery --case underscore --output k8sprometheusmock --outpkg k8sprometheusmock --name PrometheusRulesEnsurer - -func (p PrometheusOperatorCRDRepo) StoreSLOs(ctx context.Context, kmeta K8sMeta, slos []StorageSLO) error { - // Map to the Prometheus operator CRD. - rule, err := mapModelToPrometheusOperator(ctx, kmeta, slos) - if err != nil { - return fmt.Errorf("could not map model to Prometheus operator CR: %w", err) - } - - // Add object reference. - rule.ObjectMeta.OwnerReferences = append(rule.ObjectMeta.OwnerReferences, metav1.OwnerReference{ - Kind: kmeta.Kind, - APIVersion: kmeta.APIVersion, - Name: kmeta.Name, - UID: types.UID(kmeta.UID), - }) - - // Create on API server. - err = p.ensurer.EnsurePrometheusRule(ctx, rule) - if err != nil { - return fmt.Errorf("could not ensure Prometheus operator rule CR: %w", err) - } - - return nil -} diff --git a/internal/k8sprometheus/storage_test.go b/internal/k8sprometheus/storage_test.go deleted file mode 100644 index 989dff02..00000000 --- a/internal/k8sprometheus/storage_test.go +++ /dev/null @@ -1,625 +0,0 @@ -package k8sprometheus_test - -/* -import ( - "bytes" - "context" - "fmt" - "testing" - - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - "github.com/prometheus/prometheus/model/rulefmt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" - - "github.com/slok/sloth/internal/k8sprometheus" - "github.com/slok/sloth/internal/k8sprometheus/k8sprometheusmock" - "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/prometheus" - "github.com/slok/sloth/pkg/common/model" -) - -func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { - tests := map[string]struct { - k8sMeta k8sprometheus.K8sMeta - slos []k8sprometheus.StorageSLO - expYAML string - expErr bool - }{ - "Having 0 SLO rules should fail.": { - k8sMeta: k8sprometheus.K8sMeta{}, - slos: []k8sprometheus.StorageSLO{}, - expErr: true, - }, - - "Having 0 SLO rules generated should fail.": { - k8sMeta: k8sprometheus.K8sMeta{}, - slos: []k8sprometheus.StorageSLO{ - {}, - }, - expErr: true, - }, - - "Having a single SLI recording rule should render correctly.": { - k8sMeta: k8sprometheus.K8sMeta{ - Name: "test-name", - Namespace: "test-ns", - Labels: map[string]string{"lk1": "lv1"}, - Annotations: map[string]string{"ak1": "av1"}, - }, - slos: []k8sprometheus.StorageSLO{ - { - SLO: prometheus.SLO{ID: "test1"}, - Rules: prometheus.SLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - }, - }}, - }, - }, - }, - expYAML: ` ---- -# Code generated by Sloth (dev): https://github.com/slok/sloth. -# DO NOT EDIT. - -apiVersion: monitoring.coreos.com/v1 -kind: PrometheusRule -metadata: - annotations: - ak1: av1 - creationTimestamp: null - labels: - app.kubernetes.io/component: SLO - app.kubernetes.io/managed-by: sloth - lk1: lv1 - name: test-name - namespace: test-ns -spec: - groups: - - name: sloth-slo-sli-recordings-test1 - rules: - - expr: test-expr - labels: - test-label: one - record: test:record -`, - }, - - "Having a single metadata recording rule should render correctly.": { - k8sMeta: k8sprometheus.K8sMeta{ - Name: "test-name", - Namespace: "test-ns", - Labels: map[string]string{"lk1": "lv1"}, - Annotations: map[string]string{"ak1": "av1"}, - }, - slos: []k8sprometheus.StorageSLO{ - { - SLO: prometheus.SLO{ID: "test1"}, - Rules: prometheus.SLORules{ - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - }, - }}, - }, - }, - }, - expYAML: ` ---- -# Code generated by Sloth (dev): https://github.com/slok/sloth. -# DO NOT EDIT. - -apiVersion: monitoring.coreos.com/v1 -kind: PrometheusRule -metadata: - annotations: - ak1: av1 - creationTimestamp: null - labels: - app.kubernetes.io/component: SLO - app.kubernetes.io/managed-by: sloth - lk1: lv1 - name: test-name - namespace: test-ns -spec: - groups: - - name: sloth-slo-meta-recordings-test1 - rules: - - expr: test-expr - labels: - test-label: one - record: test:record -`, - }, - - "Having a single SLO alert rule should render correctly.": { - k8sMeta: k8sprometheus.K8sMeta{ - Name: "test-name", - Namespace: "test-ns", - Labels: map[string]string{"lk1": "lv1"}, - Annotations: map[string]string{"ak1": "av1"}, - }, - slos: []k8sprometheus.StorageSLO{ - { - SLO: prometheus.SLO{ID: "test1"}, - Rules: prometheus.SLORules{ - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlert", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - Annotations: map[string]string{"test-annot": "one"}, - }, - }}, - }, - }, - }, - expYAML: ` ---- -# Code generated by Sloth (dev): https://github.com/slok/sloth. -# DO NOT EDIT. - -apiVersion: monitoring.coreos.com/v1 -kind: PrometheusRule -metadata: - annotations: - ak1: av1 - creationTimestamp: null - labels: - app.kubernetes.io/component: SLO - app.kubernetes.io/managed-by: sloth - lk1: lv1 - name: test-name - namespace: test-ns -spec: - groups: - - name: sloth-slo-alerts-test1 - rules: - - alert: testAlert - annotations: - test-annot: one - expr: test-expr - labels: - test-label: one -`, - }, - - "Having a multiple SLO alert and recording rules should render correctly.": { - k8sMeta: k8sprometheus.K8sMeta{ - Name: "test-name", - Namespace: "test-ns", - Labels: map[string]string{"lk1": "lv1"}, - Annotations: map[string]string{"ak1": "av1"}, - }, - slos: []k8sprometheus.StorageSLO{ - - { - SLO: prometheus.SLO{ID: "testa"}, - Rules: prometheus.SLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-a1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - }, - { - Record: "test:record-a2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - }, - }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-a3", - Expr: "test-expr-a3", - Labels: map[string]string{"test-label": "a-3"}, - }, - { - Record: "test:record-a4", - Expr: "test-expr-a4", - Labels: map[string]string{"test-label": "a-4"}, - }, - }}, - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlertA1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - Annotations: map[string]string{"test-annot": "a-1"}, - }, - { - Alert: "testAlertA2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - Annotations: map[string]string{"test-annot": "a-2"}, - }, - }}, - }, - }, - { - SLO: prometheus.SLO{ID: "testb"}, - Rules: prometheus.SLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-b1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - }, - }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-b2", - Expr: "test-expr-b2", - Labels: map[string]string{"test-label": "b-2"}, - }, - }}, - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlertB1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - Annotations: map[string]string{"test-annot": "b-1"}, - }, - }}, - }, - }, - }, - expYAML: ` ---- -# Code generated by Sloth (dev): https://github.com/slok/sloth. -# DO NOT EDIT. - -apiVersion: monitoring.coreos.com/v1 -kind: PrometheusRule -metadata: - annotations: - ak1: av1 - creationTimestamp: null - labels: - app.kubernetes.io/component: SLO - app.kubernetes.io/managed-by: sloth - lk1: lv1 - name: test-name - namespace: test-ns -spec: - groups: - - name: sloth-slo-sli-recordings-testa - rules: - - expr: test-expr-a1 - labels: - test-label: a-1 - record: test:record-a1 - - expr: test-expr-a2 - labels: - test-label: a-2 - record: test:record-a2 - - name: sloth-slo-meta-recordings-testa - rules: - - expr: test-expr-a3 - labels: - test-label: a-3 - record: test:record-a3 - - expr: test-expr-a4 - labels: - test-label: a-4 - record: test:record-a4 - - name: sloth-slo-alerts-testa - rules: - - alert: testAlertA1 - annotations: - test-annot: a-1 - expr: test-expr-a1 - labels: - test-label: a-1 - - alert: testAlertA2 - annotations: - test-annot: a-2 - expr: test-expr-a2 - labels: - test-label: a-2 - - name: sloth-slo-sli-recordings-testb - rules: - - expr: test-expr-b1 - labels: - test-label: b-1 - record: test:record-b1 - - name: sloth-slo-meta-recordings-testb - rules: - - expr: test-expr-b2 - labels: - test-label: b-2 - record: test:record-b2 - - name: sloth-slo-alerts-testb - rules: - - alert: testAlertB1 - annotations: - test-annot: b-1 - expr: test-expr-b1 - labels: - test-label: b-1 -`, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - - var gotYAML bytes.Buffer - repo := k8sprometheus.NewIOWriterPrometheusOperatorYAMLRepo(&gotYAML, log.Noop) - err := repo.StoreSLOs(context.TODO(), test.k8sMeta, test.slos) - - if test.expErr { - assert.Error(err) - } else if assert.NoError(err) { - assert.Equal(test.expYAML, gotYAML.String()) - } - }) - } -} - -func TestPrometheusOperatorCRDRepo(t *testing.T) { - tests := map[string]struct { - k8sMeta k8sprometheus.K8sMeta - slos []k8sprometheus.StorageSLO - mock func(m *k8sprometheusmock.PrometheusRulesEnsurer) - expErr bool - }{ - "Having 0 SLO rules should fail.": { - k8sMeta: k8sprometheus.K8sMeta{}, - slos: []k8sprometheus.StorageSLO{}, - mock: func(m *k8sprometheusmock.PrometheusRulesEnsurer) {}, - expErr: true, - }, - - "Having 0 SLO rules generated should fail.": { - k8sMeta: k8sprometheus.K8sMeta{}, - slos: []k8sprometheus.StorageSLO{ - {}, - }, - mock: func(m *k8sprometheusmock.PrometheusRulesEnsurer) {}, - expErr: true, - }, - - "Having an error while storing Prometheus operator rules should fail.": { - k8sMeta: k8sprometheus.K8sMeta{}, - slos: []k8sprometheus.StorageSLO{ - { - SLO: prometheus.SLO{ID: "testa"}, - Rules: prometheus.SLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - {Record: "test:record-a1"}, - }}, - }, - }, - }, - mock: func(m *k8sprometheusmock.PrometheusRulesEnsurer) { - m.On("EnsurePrometheusRule", mock.Anything, mock.Anything).Once().Return(fmt.Errorf("something")) - }, - expErr: true, - }, - - "Having multiple SLO alert and recording rules should ensure on Kubernetes correctly.": { - k8sMeta: k8sprometheus.K8sMeta{ - Name: "test-name", - Namespace: "test-ns", - Labels: map[string]string{"lk1": "lv1"}, - Annotations: map[string]string{"ak1": "av1"}, - Kind: "test-kind", - APIVersion: "test-apiversion", - UID: "test-uid", - }, - slos: []k8sprometheus.StorageSLO{ - { - SLO: prometheus.SLO{ID: "testa"}, - Rules: prometheus.SLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-a1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - }, - { - Record: "test:record-a2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - }, - }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-a3", - Expr: "test-expr-a3", - Labels: map[string]string{"test-label": "a-3"}, - }, - { - Record: "test:record-a4", - Expr: "test-expr-a4", - Labels: map[string]string{"test-label": "a-4"}, - }, - }}, - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlertA1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - Annotations: map[string]string{"test-annot": "a-1"}, - }, - { - Alert: "testAlertA2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - Annotations: map[string]string{"test-annot": "a-2"}, - }, - }}, - }, - }, - { - SLO: prometheus.SLO{ID: "testb"}, - Rules: prometheus.SLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-b1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - }, - }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-b2", - Expr: "test-expr-b2", - Labels: map[string]string{"test-label": "b-2"}, - }, - }}, - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlertB1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - Annotations: map[string]string{"test-annot": "b-1"}, - }, - }}, - }, - }, - }, - mock: func(m *k8sprometheusmock.PrometheusRulesEnsurer) { - exp := &monitoringv1.PrometheusRule{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "monitoring.coreos.com/v1", - Kind: "PrometheusRule", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - Namespace: "test-ns", - Labels: map[string]string{ - "lk1": "lv1", - "app.kubernetes.io/component": "SLO", - "app.kubernetes.io/managed-by": "sloth", - }, - Annotations: map[string]string{"ak1": "av1"}, - OwnerReferences: []metav1.OwnerReference{ - { - Kind: "test-kind", - APIVersion: "test-apiversion", - Name: "test-name", - UID: types.UID("test-uid"), - }, - }, - }, - Spec: monitoringv1.PrometheusRuleSpec{ - Groups: []monitoringv1.RuleGroup{ - { - Name: "sloth-slo-sli-recordings-testa", - Rules: []monitoringv1.Rule{ - { - Record: "test:record-a1", - Expr: intstr.FromString("test-expr-a1"), - Labels: map[string]string{"test-label": "a-1"}, - }, - { - Record: "test:record-a2", - Expr: intstr.FromString("test-expr-a2"), - Labels: map[string]string{"test-label": "a-2"}, - }, - }, - }, - { - Name: "sloth-slo-meta-recordings-testa", - Rules: []monitoringv1.Rule{ - { - Record: "test:record-a3", - Expr: intstr.FromString("test-expr-a3"), - Labels: map[string]string{"test-label": "a-3"}, - }, - { - Record: "test:record-a4", - Expr: intstr.FromString("test-expr-a4"), - Labels: map[string]string{"test-label": "a-4"}, - }, - }, - }, - { - Name: "sloth-slo-alerts-testa", - Rules: []monitoringv1.Rule{ - { - Alert: "testAlertA1", - Expr: intstr.FromString("test-expr-a1"), - Labels: map[string]string{"test-label": "a-1"}, - Annotations: map[string]string{"test-annot": "a-1"}, - }, - { - Alert: "testAlertA2", - Expr: intstr.FromString("test-expr-a2"), - Labels: map[string]string{"test-label": "a-2"}, - Annotations: map[string]string{"test-annot": "a-2"}, - }, - }, - }, - { - Name: "sloth-slo-sli-recordings-testb", - Rules: []monitoringv1.Rule{ - { - Record: "test:record-b1", - Expr: intstr.FromString("test-expr-b1"), - Labels: map[string]string{"test-label": "b-1"}, - }, - }, - }, - { - Name: "sloth-slo-meta-recordings-testb", - Rules: []monitoringv1.Rule{ - { - Record: "test:record-b2", - Expr: intstr.FromString("test-expr-b2"), - Labels: map[string]string{"test-label": "b-2"}, - }, - }, - }, - { - Name: "sloth-slo-alerts-testb", - Rules: []monitoringv1.Rule{ - { - Alert: "testAlertB1", - Expr: intstr.FromString("test-expr-b1"), - Labels: map[string]string{"test-label": "b-1"}, - Annotations: map[string]string{"test-annot": "b-1"}, - }, - }, - }, - }, - }, - } - m.On("EnsurePrometheusRule", mock.Anything, exp).Once().Return(nil) - }, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - - // Mocks. - mpre := &k8sprometheusmock.PrometheusRulesEnsurer{} - test.mock(mpre) - - repo := k8sprometheus.NewPrometheusOperatorCRDRepo(mpre, log.Noop) - err := repo.StoreSLOs(context.TODO(), test.k8sMeta, test.slos) - - if test.expErr { - assert.Error(err) - } else { - assert.NoError(err) - } - mpre.AssertExpectations(t) - }) - } -} -*/ diff --git a/internal/kubernetes/modelmap/slo.go b/internal/kubernetes/modelmap/slo.go new file mode 100644 index 00000000..399e3083 --- /dev/null +++ b/internal/kubernetes/modelmap/slo.go @@ -0,0 +1,99 @@ +package modelmap + +import ( + "context" + "fmt" + + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + "github.com/prometheus/prometheus/model/rulefmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/slok/sloth/internal/storage" + commonerrors "github.com/slok/sloth/pkg/common/errors" +) + +func MapModelToPrometheusOperator(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) (*monitoringv1.PrometheusRule, error) { + // Add extra labels. + labels := map[string]string{ + "app.kubernetes.io/component": "SLO", + "app.kubernetes.io/managed-by": "sloth", + } + for k, v := range kmeta.Labels { + labels[k] = v + } + + rule := &monitoringv1.PrometheusRule{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "monitoring.coreos.com/v1", + Kind: "PrometheusRule", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: kmeta.Name, + Namespace: kmeta.Namespace, + Labels: labels, + Annotations: kmeta.Annotations, + }, + } + + if len(slos) == 0 { + return nil, fmt.Errorf("slo rules required") + } + + for _, slo := range slos { + if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { + rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ + Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), + Rules: promRulesToKubeRules(slo.Rules.SLIErrorRecRules.Rules), + }) + } + + if len(slo.Rules.MetadataRecRules.Rules) > 0 { + rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ + Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), + Rules: promRulesToKubeRules(slo.Rules.MetadataRecRules.Rules), + }) + } + + if len(slo.Rules.AlertRules.Rules) > 0 { + rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ + Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), + Rules: promRulesToKubeRules(slo.Rules.AlertRules.Rules), + }) + } + } + + // If we don't have anything to store, error so we can increase the reliability + // because maybe this was due to an unintended error (typos, misconfig, too many disable...). + if len(rule.Spec.Groups) == 0 { + return nil, commonerrors.ErrNoSLORules + } + + return rule, nil +} + +func promRulesToKubeRules(rules []rulefmt.Rule) []monitoringv1.Rule { + res := make([]monitoringv1.Rule, 0, len(rules)) + for _, r := range rules { + forS := "" + if r.For != 0 { + forS = r.For.String() + } + + var dur *monitoringv1.Duration + if forS != "" { + d := monitoringv1.Duration(forS) + dur = &d + } + + res = append(res, monitoringv1.Rule{ + Record: r.Record, + Alert: r.Alert, + Expr: intstr.FromString(r.Expr), + For: dur, + Labels: r.Labels, + Annotations: r.Annotations, + }) + } + return res +} diff --git a/internal/storage/io/prometheus_operator.go b/internal/storage/io/prometheus_operator.go new file mode 100644 index 00000000..6b8f466f --- /dev/null +++ b/internal/storage/io/prometheus_operator.go @@ -0,0 +1,52 @@ +package io + +import ( + "bytes" + "context" + "fmt" + "io" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + + kubernetesmodelmap "github.com/slok/sloth/internal/kubernetes/modelmap" + "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/internal/storage" +) + +func NewIOWriterPrometheusOperatorYAMLRepo(writer io.Writer, logger log.Logger) IOWriterPrometheusOperatorYAMLRepo { + return IOWriterPrometheusOperatorYAMLRepo{ + writer: writer, + encoder: json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil), + logger: logger.WithValues(log.Kv{"svc": "storage.io.IOWriterPrometheusOperatorYAMLRepo"}), + } +} + +// IOWriterPrometheusOperatorYAMLRepo knows to store all the SLO rules (recordings and alerts) +// grouped in an IOWriter in Kubernetes prometheus operator YAML format. +type IOWriterPrometheusOperatorYAMLRepo struct { + writer io.Writer + encoder runtime.Encoder + logger log.Logger +} + +func (i IOWriterPrometheusOperatorYAMLRepo) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error { + rule, err := kubernetesmodelmap.MapModelToPrometheusOperator(ctx, kmeta, slos) + if err != nil { + return fmt.Errorf("could not map model to Prometheus operator CR: %w", err) + } + + var b bytes.Buffer + err = i.encoder.Encode(rule, &b) + if err != nil { + return fmt.Errorf("could encode prometheus operator object: %w", err) + } + + rulesYaml := writeYAMLTopDisclaimer(b.Bytes()) + _, err = i.writer.Write(rulesYaml) + if err != nil { + return fmt.Errorf("could not write top disclaimer: %w", err) + } + + return nil +} diff --git a/internal/storage/io/prometheus_operator_test.go b/internal/storage/io/prometheus_operator_test.go new file mode 100644 index 00000000..7f2ccdff --- /dev/null +++ b/internal/storage/io/prometheus_operator_test.go @@ -0,0 +1,361 @@ +package io_test + +import ( + "bytes" + "context" + "testing" + + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/stretchr/testify/assert" + + "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/internal/storage" + "github.com/slok/sloth/internal/storage/io" + "github.com/slok/sloth/pkg/common/model" +) + +func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { + tests := map[string]struct { + k8sMeta storage.K8sMeta + slos []storage.SLORulesResult + expYAML string + expErr bool + }{ + "Having 0 SLO rules should fail.": { + k8sMeta: storage.K8sMeta{}, + slos: []storage.SLORulesResult{}, + expErr: true, + }, + + "Having 0 SLO rules generated should fail.": { + k8sMeta: storage.K8sMeta{}, + slos: []storage.SLORulesResult{ + {}, + }, + expErr: true, + }, + + "Having a single SLI recording rule should render correctly.": { + k8sMeta: storage.K8sMeta{ + Name: "test-name", + Namespace: "test-ns", + Labels: map[string]string{"lk1": "lv1"}, + Annotations: map[string]string{"ak1": "av1"}, + }, + slos: []storage.SLORulesResult{ + { + SLO: prometheus.SLO{ID: "test1"}, + Rules: prometheus.SLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "test:record", + Expr: "test-expr", + Labels: map[string]string{"test-label": "one"}, + }, + }}, + }, + }, + }, + expYAML: ` +--- +# Code generated by Sloth (dev): https://github.com/slok/sloth. +# DO NOT EDIT. + +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + annotations: + ak1: av1 + creationTimestamp: null + labels: + app.kubernetes.io/component: SLO + app.kubernetes.io/managed-by: sloth + lk1: lv1 + name: test-name + namespace: test-ns +spec: + groups: + - name: sloth-slo-sli-recordings-test1 + rules: + - expr: test-expr + labels: + test-label: one + record: test:record +`, + }, + + "Having a single metadata recording rule should render correctly.": { + k8sMeta: storage.K8sMeta{ + Name: "test-name", + Namespace: "test-ns", + Labels: map[string]string{"lk1": "lv1"}, + Annotations: map[string]string{"ak1": "av1"}, + }, + slos: []storage.SLORulesResult{ + { + SLO: prometheus.SLO{ID: "test1"}, + Rules: prometheus.SLORules{ + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "test:record", + Expr: "test-expr", + Labels: map[string]string{"test-label": "one"}, + }, + }}, + }, + }, + }, + expYAML: ` +--- +# Code generated by Sloth (dev): https://github.com/slok/sloth. +# DO NOT EDIT. + +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + annotations: + ak1: av1 + creationTimestamp: null + labels: + app.kubernetes.io/component: SLO + app.kubernetes.io/managed-by: sloth + lk1: lv1 + name: test-name + namespace: test-ns +spec: + groups: + - name: sloth-slo-meta-recordings-test1 + rules: + - expr: test-expr + labels: + test-label: one + record: test:record +`, + }, + + "Having a single SLO alert rule should render correctly.": { + k8sMeta: storage.K8sMeta{ + Name: "test-name", + Namespace: "test-ns", + Labels: map[string]string{"lk1": "lv1"}, + Annotations: map[string]string{"ak1": "av1"}, + }, + slos: []storage.SLORulesResult{ + { + SLO: prometheus.SLO{ID: "test1"}, + Rules: prometheus.SLORules{ + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Alert: "testAlert", + Expr: "test-expr", + Labels: map[string]string{"test-label": "one"}, + Annotations: map[string]string{"test-annot": "one"}, + }, + }}, + }, + }, + }, + expYAML: ` +--- +# Code generated by Sloth (dev): https://github.com/slok/sloth. +# DO NOT EDIT. + +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + annotations: + ak1: av1 + creationTimestamp: null + labels: + app.kubernetes.io/component: SLO + app.kubernetes.io/managed-by: sloth + lk1: lv1 + name: test-name + namespace: test-ns +spec: + groups: + - name: sloth-slo-alerts-test1 + rules: + - alert: testAlert + annotations: + test-annot: one + expr: test-expr + labels: + test-label: one +`, + }, + + "Having a multiple SLO alert and recording rules should render correctly.": { + k8sMeta: storage.K8sMeta{ + Name: "test-name", + Namespace: "test-ns", + Labels: map[string]string{"lk1": "lv1"}, + Annotations: map[string]string{"ak1": "av1"}, + }, + slos: []storage.SLORulesResult{ + + { + SLO: prometheus.SLO{ID: "testa"}, + Rules: prometheus.SLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "test:record-a1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + }, + { + Record: "test:record-a2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "test:record-a3", + Expr: "test-expr-a3", + Labels: map[string]string{"test-label": "a-3"}, + }, + { + Record: "test:record-a4", + Expr: "test-expr-a4", + Labels: map[string]string{"test-label": "a-4"}, + }, + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Alert: "testAlertA1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + Annotations: map[string]string{"test-annot": "a-1"}, + }, + { + Alert: "testAlertA2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + Annotations: map[string]string{"test-annot": "a-2"}, + }, + }}, + }, + }, + { + SLO: prometheus.SLO{ID: "testb"}, + Rules: prometheus.SLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "test:record-b1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "test:record-b2", + Expr: "test-expr-b2", + Labels: map[string]string{"test-label": "b-2"}, + }, + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Alert: "testAlertB1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + Annotations: map[string]string{"test-annot": "b-1"}, + }, + }}, + }, + }, + }, + expYAML: ` +--- +# Code generated by Sloth (dev): https://github.com/slok/sloth. +# DO NOT EDIT. + +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + annotations: + ak1: av1 + creationTimestamp: null + labels: + app.kubernetes.io/component: SLO + app.kubernetes.io/managed-by: sloth + lk1: lv1 + name: test-name + namespace: test-ns +spec: + groups: + - name: sloth-slo-sli-recordings-testa + rules: + - expr: test-expr-a1 + labels: + test-label: a-1 + record: test:record-a1 + - expr: test-expr-a2 + labels: + test-label: a-2 + record: test:record-a2 + - name: sloth-slo-meta-recordings-testa + rules: + - expr: test-expr-a3 + labels: + test-label: a-3 + record: test:record-a3 + - expr: test-expr-a4 + labels: + test-label: a-4 + record: test:record-a4 + - name: sloth-slo-alerts-testa + rules: + - alert: testAlertA1 + annotations: + test-annot: a-1 + expr: test-expr-a1 + labels: + test-label: a-1 + - alert: testAlertA2 + annotations: + test-annot: a-2 + expr: test-expr-a2 + labels: + test-label: a-2 + - name: sloth-slo-sli-recordings-testb + rules: + - expr: test-expr-b1 + labels: + test-label: b-1 + record: test:record-b1 + - name: sloth-slo-meta-recordings-testb + rules: + - expr: test-expr-b2 + labels: + test-label: b-2 + record: test:record-b2 + - name: sloth-slo-alerts-testb + rules: + - alert: testAlertB1 + annotations: + test-annot: b-1 + expr: test-expr-b1 + labels: + test-label: b-1 +`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + var gotYAML bytes.Buffer + repo := io.NewIOWriterPrometheusOperatorYAMLRepo(&gotYAML, log.Noop) + err := repo.StoreSLOs(context.TODO(), test.k8sMeta, test.slos) + + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expYAML, gotYAML.String()) + } + }) + } +} diff --git a/internal/storage/k8s/dry_run.go b/internal/storage/k8s/dry_run.go new file mode 100644 index 00000000..2a03216f --- /dev/null +++ b/internal/storage/k8s/dry_run.go @@ -0,0 +1,43 @@ +package k8s + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" + + "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/internal/storage" + slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" +) + +type DryRunApiserverRepository struct { + svc ApiserverRepository + logger log.Logger +} + +// NewDryRunApiserverRepository returns a new Kubernetes Service that will dry-run that will only do real ReadOnly operations. +func NewDryRunApiserverRepository(svc ApiserverRepository, logger log.Logger) DryRunApiserverRepository { + return DryRunApiserverRepository{ + svc: svc, + logger: logger.WithValues(log.Kv{"service": "storage.k8s.DryRunApiserverRepository"}), + } +} + +func (r DryRunApiserverRepository) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { + return r.svc.ListPrometheusServiceLevels(ctx, ns, opts) +} + +func (r DryRunApiserverRepository) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) { + return r.svc.WatchPrometheusServiceLevels(ctx, ns, opts) +} + +func (r DryRunApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error { + r.logger.Infof("Dry run EnsurePrometheusServiceLevelStatus") + return nil +} + +func (r DryRunApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error { + r.logger.Infof("Dry run StoreSLOs") + return nil +} diff --git a/internal/storage/k8s/fake.go b/internal/storage/k8s/fake.go new file mode 100644 index 00000000..a65066de --- /dev/null +++ b/internal/storage/k8s/fake.go @@ -0,0 +1,102 @@ +package k8s + +import ( + "context" + + monitoringclientsetfake "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + + "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/internal/storage" + slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" + slothclientsetfake "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/fake" +) + +type FakeApiserverRepository struct { + ksvc ApiserverRepository +} + +// NewFakeApiserverRepository returns a new Kubernetes Service that will fake Kubernetes operations +// using fake clients. +func NewFakeApiserverRepository(logger log.Logger) FakeApiserverRepository { + return FakeApiserverRepository{ + ksvc: NewApiserverRepository( + slothclientsetfake.NewSimpleClientset(prometheusServiceLevelFakes...), + monitoringclientsetfake.NewSimpleClientset(), + logger), + } +} + +func (r FakeApiserverRepository) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { + return r.ksvc.ListPrometheusServiceLevels(ctx, ns, opts) +} + +func (r FakeApiserverRepository) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) { + return r.ksvc.WatchPrometheusServiceLevels(ctx, ns, opts) +} + +func (r FakeApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error { + return r.ksvc.EnsurePrometheusServiceLevelStatus(ctx, slo, err) +} + +func (r FakeApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error { + return r.ksvc.StoreSLOs(ctx, kmeta, slos) +} + +var prometheusServiceLevelFakes = []runtime.Object{ + &slothv1.PrometheusServiceLevel{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fake01", + Labels: map[string]string{ + "prometheus": "default", + }, + }, + Spec: slothv1.PrometheusServiceLevelSpec{ + Service: "svc01", + Labels: map[string]string{ + "globalk1": "globalv1", + }, + SLOs: []slothv1.SLO{ + { + Name: "slo01", + Objective: 99.9, + Labels: map[string]string{ + "slo01k1": "slo01v1", + }, + SLI: slothv1.SLI{Events: &slothv1.SLIEvents{ + ErrorQuery: `sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}]))`, + TotalQuery: `sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}]))`, + }}, + Alerting: slothv1.Alerting{ + Name: "myServiceAlert", + Labels: map[string]string{ + "alert01k1": "alert01v1", + }, + Annotations: map[string]string{ + "alert02k1": "alert02v1", + }, + PageAlert: slothv1.Alert{}, + TicketAlert: slothv1.Alert{}, + }, + }, + { + Name: "slo02", + Objective: 99.99, + SLI: slothv1.SLI{Raw: &slothv1.SLIRaw{ + ErrorRatioQuery: ` +sum(rate(http_request_duration_seconds_count{job="myservice2",code=~"(5..|429)"}[{{.window}}])) +/ +sum(rate(http_request_duration_seconds_count{job="myservice2"}[{{.window}}])) +`, + }}, + Alerting: slothv1.Alerting{ + PageAlert: slothv1.Alert{Disable: true}, + TicketAlert: slothv1.Alert{Disable: true}, + }, + }, + }, + }, + }, +} diff --git a/internal/storage/k8s/k8s.go b/internal/storage/k8s/k8s.go index cbbb5f83..0e0ed21c 100644 --- a/internal/storage/k8s/k8s.go +++ b/internal/storage/k8s/k8s.go @@ -2,20 +2,21 @@ package k8s import ( "context" + "fmt" "time" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" monitoringclientset "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" - monitoringclientsetfake "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" kubeerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" + kubernetesmodelmap "github.com/slok/sloth/internal/kubernetes/modelmap" "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/internal/storage" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientset "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" - slothclientsetfake "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/fake" ) type ApiserverRepository struct { @@ -41,34 +42,6 @@ func (r ApiserverRepository) WatchPrometheusServiceLevels(ctx context.Context, n return r.slothCli.SlothV1().PrometheusServiceLevels(ns).Watch(ctx, opts) } -func (r ApiserverRepository) EnsurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error { - logger := r.logger.WithCtxValues(ctx) - pr = pr.DeepCopy() - stored, err := r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Get(ctx, pr.Name, metav1.GetOptions{}) - if err != nil { - if !kubeerrors.IsNotFound(err) { - return err - } - _, err = r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Create(ctx, pr, metav1.CreateOptions{}) - if err != nil { - return err - } - logger.Debugf("monitoringv1.PrometheusRule has been created") - - return nil - } - - // Force overwrite. - pr.ObjectMeta.ResourceVersion = stored.ResourceVersion - _, err = r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Update(ctx, pr, metav1.UpdateOptions{}) - if err != nil { - return err - } - logger.Debugf("monitoringv1.PrometheusRule has been overwritten") - - return nil -} - // EnsurePrometheusServiceLevelStatus updates the status of a PrometheusServiceLeve, be aware that updating // an status will trigger a watch update event on a controller. // In case of no error we will update "last correct Prometheus operation rules generated" TS so we can be in @@ -91,120 +64,55 @@ func (r ApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Cont return err } -type DryRunApiserverRepository struct { - svc ApiserverRepository - logger log.Logger -} - -// NewDryRunApiserverRepository returns a new Kubernetes Service that will dry-run that will only do real ReadOnly operations. -func NewDryRunApiserverRepository(svc ApiserverRepository, logger log.Logger) DryRunApiserverRepository { - return DryRunApiserverRepository{ - svc: svc, - logger: logger.WithValues(log.Kv{"service": "storage.k8s.DryRunApiserverRepository"}), +func (r ApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error { + // Map to the Prometheus operator CRD. + rule, err := kubernetesmodelmap.MapModelToPrometheusOperator(ctx, kmeta, slos) + if err != nil { + return fmt.Errorf("could not map model to Prometheus operator CR: %w", err) + } + fmt.Println(rule) + + // Add object reference. + rule.ObjectMeta.OwnerReferences = append(rule.ObjectMeta.OwnerReferences, metav1.OwnerReference{ + Kind: kmeta.Kind, + APIVersion: kmeta.APIVersion, + Name: kmeta.Name, + UID: types.UID(kmeta.UID), + }) + + // Create on API server. + err = r.ensurePrometheusRule(ctx, rule) + if err != nil { + return fmt.Errorf("could not ensure Prometheus operator rule CR: %w", err) } -} - -func (r DryRunApiserverRepository) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { - return r.svc.ListPrometheusServiceLevels(ctx, ns, opts) -} - -func (r DryRunApiserverRepository) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) { - return r.svc.WatchPrometheusServiceLevels(ctx, ns, opts) -} - -func (r DryRunApiserverRepository) EnsurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error { - r.logger.Infof("Dry run EnsurePrometheusRule") - return nil -} -func (r DryRunApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error { - r.logger.Infof("Dry run EnsurePrometheusServiceLevelStatus") return nil } -type FakeApiserverRepository struct { - ksvc ApiserverRepository -} +func (r ApiserverRepository) ensurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error { + logger := r.logger.WithCtxValues(ctx) + pr = pr.DeepCopy() + stored, err := r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Get(ctx, pr.Name, metav1.GetOptions{}) + if err != nil { + if !kubeerrors.IsNotFound(err) { + return err + } + _, err = r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Create(ctx, pr, metav1.CreateOptions{}) + if err != nil { + return err + } + logger.Debugf("monitoringv1.PrometheusRule has been created") -// NewFakeApiserverRepository returns a new Kubernetes Service that will fake Kubernetes operations -// using fake clients. -func NewFakeApiserverRepository(logger log.Logger) FakeApiserverRepository { - return FakeApiserverRepository{ - ksvc: NewApiserverRepository( - slothclientsetfake.NewSimpleClientset(prometheusServiceLevelFakes...), - monitoringclientsetfake.NewSimpleClientset(), - logger), + return nil } -} -func (r FakeApiserverRepository) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { - return r.ksvc.ListPrometheusServiceLevels(ctx, ns, opts) -} - -func (r FakeApiserverRepository) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) { - return r.ksvc.WatchPrometheusServiceLevels(ctx, ns, opts) -} - -func (r FakeApiserverRepository) EnsurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error { - return r.ksvc.EnsurePrometheusRule(ctx, pr) -} - -func (r FakeApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error { - return r.ksvc.EnsurePrometheusServiceLevelStatus(ctx, slo, err) -} + // Force overwrite. + pr.ObjectMeta.ResourceVersion = stored.ResourceVersion + _, err = r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Update(ctx, pr, metav1.UpdateOptions{}) + if err != nil { + return err + } + logger.Debugf("monitoringv1.PrometheusRule has been overwritten") -var prometheusServiceLevelFakes = []runtime.Object{ - &slothv1.PrometheusServiceLevel{ - ObjectMeta: metav1.ObjectMeta{ - Name: "fake01", - Labels: map[string]string{ - "prometheus": "default", - }, - }, - Spec: slothv1.PrometheusServiceLevelSpec{ - Service: "svc01", - Labels: map[string]string{ - "globalk1": "globalv1", - }, - SLOs: []slothv1.SLO{ - { - Name: "slo01", - Objective: 99.9, - Labels: map[string]string{ - "slo01k1": "slo01v1", - }, - SLI: slothv1.SLI{Events: &slothv1.SLIEvents{ - ErrorQuery: `sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}]))`, - TotalQuery: `sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}]))`, - }}, - Alerting: slothv1.Alerting{ - Name: "myServiceAlert", - Labels: map[string]string{ - "alert01k1": "alert01v1", - }, - Annotations: map[string]string{ - "alert02k1": "alert02v1", - }, - PageAlert: slothv1.Alert{}, - TicketAlert: slothv1.Alert{}, - }, - }, - { - Name: "slo02", - Objective: 99.99, - SLI: slothv1.SLI{Raw: &slothv1.SLIRaw{ - ErrorRatioQuery: ` -sum(rate(http_request_duration_seconds_count{job="myservice2",code=~"(5..|429)"}[{{.window}}])) -/ -sum(rate(http_request_duration_seconds_count{job="myservice2"}[{{.window}}])) -`, - }}, - Alerting: slothv1.Alerting{ - PageAlert: slothv1.Alert{Disable: true}, - TicketAlert: slothv1.Alert{Disable: true}, - }, - }, - }, - }, - }, + return nil } diff --git a/internal/storage/k8s/k8s_test.go b/internal/storage/k8s/k8s_test.go new file mode 100644 index 00000000..fa354094 --- /dev/null +++ b/internal/storage/k8s/k8s_test.go @@ -0,0 +1,262 @@ +package k8s_test + +import ( + "context" + "testing" + + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + monitoringclientsetfake "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/internal/prometheus" + "github.com/slok/sloth/internal/storage" + storagek8s "github.com/slok/sloth/internal/storage/k8s" + "github.com/slok/sloth/pkg/common/model" + slothclientsetfake "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/fake" +) + +func TestApiserverRepositoryStoreSLOs(t *testing.T) { + tests := map[string]struct { + k8sMeta storage.K8sMeta + slos []storage.SLORulesResult + expPromOperatorRules []monitoringv1.PrometheusRule + expErr bool + }{ + "Having 0 SLO rules should fail.": { + k8sMeta: storage.K8sMeta{}, + slos: []storage.SLORulesResult{}, + expErr: true, + }, + + "Having 0 SLO rules generated should fail.": { + k8sMeta: storage.K8sMeta{}, + slos: []storage.SLORulesResult{ + {}, + }, + expErr: true, + }, + + "Having multiple SLO alert and recording rules should ensure on Kubernetes correctly.": { + k8sMeta: storage.K8sMeta{ + Name: "test-name", + Namespace: "test-ns", + Labels: map[string]string{"lk1": "lv1"}, + Annotations: map[string]string{"ak1": "av1"}, + Kind: "test-kind", + APIVersion: "test-apiversion", + UID: "test-uid", + }, + slos: []storage.SLORulesResult{ + { + SLO: prometheus.SLO{ID: "testa"}, + Rules: prometheus.SLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "test:record-a1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + }, + { + Record: "test:record-a2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "test:record-a3", + Expr: "test-expr-a3", + Labels: map[string]string{"test-label": "a-3"}, + }, + { + Record: "test:record-a4", + Expr: "test-expr-a4", + Labels: map[string]string{"test-label": "a-4"}, + }, + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Alert: "testAlertA1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + Annotations: map[string]string{"test-annot": "a-1"}, + }, + { + Alert: "testAlertA2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + Annotations: map[string]string{"test-annot": "a-2"}, + }, + }}, + }, + }, + { + SLO: prometheus.SLO{ID: "testb"}, + Rules: prometheus.SLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "test:record-b1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "test:record-b2", + Expr: "test-expr-b2", + Labels: map[string]string{"test-label": "b-2"}, + }, + }}, + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Alert: "testAlertB1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + Annotations: map[string]string{"test-annot": "b-1"}, + }, + }}, + }, + }, + }, + expPromOperatorRules: []monitoringv1.PrometheusRule{ + { + TypeMeta: metav1.TypeMeta{ + APIVersion: "monitoring.coreos.com/v1", + Kind: "PrometheusRule", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Namespace: "test-ns", + Labels: map[string]string{ + "lk1": "lv1", + "app.kubernetes.io/component": "SLO", + "app.kubernetes.io/managed-by": "sloth", + }, + Annotations: map[string]string{"ak1": "av1"}, + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "test-kind", + APIVersion: "test-apiversion", + Name: "test-name", + UID: types.UID("test-uid"), + }, + }, + }, + Spec: monitoringv1.PrometheusRuleSpec{ + Groups: []monitoringv1.RuleGroup{ + { + Name: "sloth-slo-sli-recordings-testa", + Rules: []monitoringv1.Rule{ + { + Record: "test:record-a1", + Expr: intstr.FromString("test-expr-a1"), + Labels: map[string]string{"test-label": "a-1"}, + }, + { + Record: "test:record-a2", + Expr: intstr.FromString("test-expr-a2"), + Labels: map[string]string{"test-label": "a-2"}, + }, + }, + }, + { + Name: "sloth-slo-meta-recordings-testa", + Rules: []monitoringv1.Rule{ + { + Record: "test:record-a3", + Expr: intstr.FromString("test-expr-a3"), + Labels: map[string]string{"test-label": "a-3"}, + }, + { + Record: "test:record-a4", + Expr: intstr.FromString("test-expr-a4"), + Labels: map[string]string{"test-label": "a-4"}, + }, + }, + }, + { + Name: "sloth-slo-alerts-testa", + Rules: []monitoringv1.Rule{ + { + Alert: "testAlertA1", + Expr: intstr.FromString("test-expr-a1"), + Labels: map[string]string{"test-label": "a-1"}, + Annotations: map[string]string{"test-annot": "a-1"}, + }, + { + Alert: "testAlertA2", + Expr: intstr.FromString("test-expr-a2"), + Labels: map[string]string{"test-label": "a-2"}, + Annotations: map[string]string{"test-annot": "a-2"}, + }, + }, + }, + { + Name: "sloth-slo-sli-recordings-testb", + Rules: []monitoringv1.Rule{ + { + Record: "test:record-b1", + Expr: intstr.FromString("test-expr-b1"), + Labels: map[string]string{"test-label": "b-1"}, + }, + }, + }, + { + Name: "sloth-slo-meta-recordings-testb", + Rules: []monitoringv1.Rule{ + { + Record: "test:record-b2", + Expr: intstr.FromString("test-expr-b2"), + Labels: map[string]string{"test-label": "b-2"}, + }, + }, + }, + { + Name: "sloth-slo-alerts-testb", + Rules: []monitoringv1.Rule{ + { + Alert: "testAlertB1", + Expr: intstr.FromString("test-expr-b1"), + Labels: map[string]string{"test-label": "b-1"}, + Annotations: map[string]string{"test-annot": "b-1"}, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Change to NewClientset when https://github.com/kubernetes/kubernetes/issues/126850 fixed. + slothCLI := slothclientsetfake.NewSimpleClientset() + promOpCli := monitoringclientsetfake.NewSimpleClientset() + + repo := storagek8s.NewApiserverRepository(slothCLI, promOpCli, log.Noop) + err := repo.StoreSLOs(context.TODO(), test.k8sMeta, test.slos) + + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + gotPromRules, err := promOpCli.MonitoringV1().PrometheusRules("").List(t.Context(), v1.ListOptions{}) + require.NoError(err) + + assert.Equal(test.expPromOperatorRules, gotPromRules.Items) + + } + }) + } +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 00000000..049eeea4 --- /dev/null +++ b/internal/storage/storage.go @@ -0,0 +1,21 @@ +package storage + +import "github.com/slok/sloth/pkg/common/model" + +// K8sMeta is the Kubernetes metadata simplified used for storage purposes. +type K8sMeta struct { + Kind string `validate:"required"` + APIVersion string `validate:"required"` + Name string `validate:"required"` + UID string + Namespace string + Annotations map[string]string + Labels map[string]string +} + +// SLORulesResult is a common type used to store final SLO rules result in batches. +type SLORulesResult struct { + K8sMeta K8sMeta + SLO model.PromSLO + Rules model.PromSLORules +} diff --git a/pkg/common/errors/errors.go b/pkg/common/errors/errors.go new file mode 100644 index 00000000..161218ec --- /dev/null +++ b/pkg/common/errors/errors.go @@ -0,0 +1,9 @@ +package errors + +import "fmt" + +var ( + // ErrNoSLORules will be used when there are no rules to store. The upper layer + // could ignore or handle the error in cases where there wasn't an output. + ErrNoSLORules = fmt.Errorf("0 SLO Prometheus rules generated") +) From 6d679c142121326e4a33f9dba2685801065a280c Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Tue, 1 Apr 2025 17:16:08 +0200 Subject: [PATCH 033/173] Add SLO generation as a chain of processors Signed-off-by: Xabier Larrakoetxea --- internal/app/generate/process.go | 27 +++++++++++++ internal/app/generate/process_core.go | 47 +++++++++++++++++++++++ internal/app/generate/prometheus.go | 48 ++++++++++-------------- internal/app/generate/prometheus_test.go | 35 ----------------- 4 files changed, 93 insertions(+), 64 deletions(-) create mode 100644 internal/app/generate/process.go create mode 100644 internal/app/generate/process_core.go diff --git a/internal/app/generate/process.go b/internal/app/generate/process.go new file mode 100644 index 00000000..d9e311f7 --- /dev/null +++ b/internal/app/generate/process.go @@ -0,0 +1,27 @@ +package generate + +import ( + "context" + + "github.com/slok/sloth/pkg/common/model" +) + +type sloProcessortRequest struct { + Info model.Info + SLO model.PromSLO + Alerts model.MWMBAlertGroup +} +type sloProcessortResult struct { + SLORules model.PromSLORules +} + +type sloProcessor interface { + ProcessSLO(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error +} + +// sloProcessorFunc is a helper function to create processors easily. +type sloProcessorFunc func(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error + +func (s sloProcessorFunc) ProcessSLO(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error { + return s(ctx, req, res) +} diff --git a/internal/app/generate/process_core.go b/internal/app/generate/process_core.go new file mode 100644 index 00000000..2268bafc --- /dev/null +++ b/internal/app/generate/process_core.go @@ -0,0 +1,47 @@ +package generate + +import ( + "context" + "fmt" + + "github.com/slok/sloth/internal/log" +) + +func newProcessorForSLIRecordingRulesGenerator(logger log.Logger, gen SLIRecordingRulesGenerator) sloProcessor { + return sloProcessorFunc(func(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error { + rules, err := gen.GenerateSLIRecordingRules(ctx, req.SLO, req.Alerts) + if err != nil { + return fmt.Errorf("could not generate Prometheus sli recording rules: %w", err) + } + res.SLORules.SLIErrorRecRules.Rules = rules + + logger.WithValues(log.Kv{"rules": len(rules)}).Infof("SLI recording rules generated") + return nil + }) +} + +func newProcessorForMetaRecordingRulesGenerator(logger log.Logger, gen MetadataRecordingRulesGenerator) sloProcessor { + return sloProcessorFunc(func(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error { + rules, err := gen.GenerateMetadataRecordingRules(ctx, req.Info, req.SLO, req.Alerts) + if err != nil { + return fmt.Errorf("could not generate Prometheus metadata recording rules: %w", err) + } + res.SLORules.MetadataRecRules.Rules = rules + + logger.WithValues(log.Kv{"rules": len(rules)}).Infof("Metadata recording rules generated") + return nil + }) +} + +func newProcessorForSLOAlertRulesGenerator(logger log.Logger, gen SLOAlertRulesGenerator) sloProcessor { + return sloProcessorFunc(func(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error { + rules, err := gen.GenerateSLOAlertRules(ctx, req.SLO, req.Alerts) + if err != nil { + return fmt.Errorf("could not generate Prometheus alert rules: %w", err) + } + res.SLORules.AlertRules.Rules = rules + + logger.WithValues(log.Kv{"rules": len(rules)}).Infof("SLO alert rules generated") + return nil + }) +} diff --git a/internal/app/generate/prometheus.go b/internal/app/generate/prometheus.go index d8a7b588..60d54509 100644 --- a/internal/app/generate/prometheus.go +++ b/internal/app/generate/prometheus.go @@ -103,7 +103,6 @@ type Request struct { type SLOResult struct { SLO model.PromSLO - Alerts model.MWMBAlertGroup SLORules model.PromSLORules } @@ -129,7 +128,7 @@ func (s Service) Generate(ctx context.Context, r Request) (*Response, error) { return nil, fmt.Errorf("could not generate %q slo: %w", slo.ID, err) } - results = append(results, *result) + results = append(results, SLOResult{SLO: slo, SLORules: *result}) } return &Response{ @@ -137,7 +136,7 @@ func (s Service) Generate(ctx context.Context, r Request) (*Response, error) { }, nil } -func (s Service) generateSLO(ctx context.Context, info model.Info, slo prometheus.SLO) (*SLOResult, error) { +func (s Service) generateSLO(ctx context.Context, info model.Info, slo prometheus.SLO) (*model.PromSLORules, error) { logger := s.logger.WithCtxValues(ctx).WithValues(log.Kv{"slo": slo.ID}) // Generate the MWMB alerts. @@ -150,36 +149,27 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, slo prometheu if err != nil { return nil, fmt.Errorf("could not generate SLO alerts: %w", err) } - logger.Infof("Multiwindow-multiburn alerts generated") + logger.Debugf("Multiwindow-multiburn alerts generated") - // Generate SLI recording rules. - sliRecordingRules, err := s.sliRecordRuleGen.GenerateSLIRecordingRules(ctx, slo, *as) - if err != nil { - return nil, fmt.Errorf("could not generate Prometheus sli recording rules: %w", err) + // Set default processors. + processors := []sloProcessor{ + newProcessorForMetaRecordingRulesGenerator(logger, s.metaRecordRuleGen), + newProcessorForSLIRecordingRulesGenerator(logger, s.sliRecordRuleGen), + newProcessorForSLOAlertRulesGenerator(logger, s.alertRuleGen), } - logger.WithValues(log.Kv{"rules": len(sliRecordingRules)}).Infof("SLI recording rules generated") - // Generate Metadata recording rules. - metaRecordingRules, err := s.metaRecordRuleGen.GenerateMetadataRecordingRules(ctx, info, slo, *as) - if err != nil { - return nil, fmt.Errorf("could not generate Prometheus metadata recording rules: %w", err) + req := &sloProcessortRequest{ + Info: info, + Alerts: *as, + SLO: slo, } - logger.WithValues(log.Kv{"rules": len(metaRecordingRules)}).Infof("Metadata recording rules generated") - - // Generate Alert rules. - alertRules, err := s.alertRuleGen.GenerateSLOAlertRules(ctx, slo, *as) - if err != nil { - return nil, fmt.Errorf("could not generate Prometheus alert rules: %w", err) + res := &sloProcessortResult{} + for _, p := range processors { + err := p.ProcessSLO(ctx, req, res) + if err != nil { + return nil, fmt.Errorf("slo processor failed: %w", err) + } } - logger.WithValues(log.Kv{"rules": len(alertRules)}).Infof("SLO alert rules generated") - return &SLOResult{ - SLO: slo, - Alerts: *as, - SLORules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: sliRecordingRules}, - MetadataRecRules: model.PromRuleGroup{Rules: metaRecordingRules}, - AlertRules: model.PromRuleGroup{Rules: alertRules}, - }, - }, nil + return &res.SLORules, nil } diff --git a/internal/app/generate/prometheus_test.go b/internal/app/generate/prometheus_test.go index c9db36c1..2b57ad64 100644 --- a/internal/app/generate/prometheus_test.go +++ b/internal/app/generate/prometheus_test.go @@ -94,41 +94,6 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { Annotations: map[string]string{"t_alert_annot": "t_label_an_1"}, }, }, - Alerts: model.MWMBAlertGroup{ - PageQuick: model.MWMBAlert{ - ID: "test-id-page-quick", - ShortWindow: 5 * time.Minute, - LongWindow: 1 * time.Hour, - BurnRateFactor: 14.4, - ErrorBudget: 0.09999999999999432, - Severity: model.PageAlertSeverity, - }, - PageSlow: model.MWMBAlert{ - ID: "test-id-page-slow", - ShortWindow: 30 * time.Minute, - LongWindow: 6 * time.Hour, - BurnRateFactor: 6, - ErrorBudget: 0.09999999999999432, - Severity: model.PageAlertSeverity, - }, - - TicketQuick: model.MWMBAlert{ - ID: "test-id-ticket-quick", - ShortWindow: 2 * time.Hour, - LongWindow: 1 * 24 * time.Hour, - BurnRateFactor: 3, - ErrorBudget: 0.09999999999999432, - Severity: model.TicketAlertSeverity, - }, - TicketSlow: model.MWMBAlert{ - ID: "test-id-ticket-slow", - ShortWindow: 6 * time.Hour, - LongWindow: 3 * 24 * time.Hour, - BurnRateFactor: 1, - ErrorBudget: 0.09999999999999432, - Severity: model.TicketAlertSeverity, - }, - }, SLORules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { From 7d06caec0f41098791c23b7434245865b0d046c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 07:05:18 +0000 Subject: [PATCH 034/173] build(deps): bump golang in /docker/prod Bumps golang from 1.24.1-alpine to 1.24.2-alpine. --- updated-dependencies: - dependency-name: golang dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docker/prod/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index a54801ad..5a930241 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,7 +1,7 @@ # Set also `ARCH` ARG here so we can use it on all the `FROM`s. ARG ARCH -FROM golang:1.24.1-alpine as build-stage +FROM golang:1.24.2-alpine as build-stage LABEL org.opencontainers.image.source https://github.com/slok/sloth From 75b9fe1532056dcef3f4090137a2de1f71d6071f Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Thu, 3 Apr 2025 08:57:19 +0200 Subject: [PATCH 035/173] Add SLO rule generation plugin system Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 1 + cmd/sloth/commands/generate.go | 59 ++-- cmd/sloth/commands/k8scontroller.go | 47 ++- docker/dev/Dockerfile | 8 +- .../generate/{prometheus.go => generate.go} | 125 ++++---- .../{prometheus_test.go => generate_test.go} | 0 internal/app/generate/noop.go | 32 -- internal/app/generate/process.go | 66 +++- internal/app/generate/process_core.go | 47 --- .../slo/core/alert_rules_v1/plugin.go} | 37 ++- .../slo/core/alert_rules_v1/plugin_test.go} | 118 +++++-- .../slo/core/metadata_rules_v1/plugin.go | 145 +++++++++ .../slo/core/metadata_rules_v1/plugin_test.go | 220 +++++++++++++ internal/plugin/slo/core/noop_v1/plugin.go | 23 ++ .../plugin/slo/core/noop_v1/plugin_test.go | 74 +++++ .../slo/core/sli_rules_v1/plugin.go} | 196 ++++-------- .../slo/core/sli_rules_v1/plugin_test.go} | 271 ++++++---------- internal/plugin/slo/custom/custom.go | 11 + .../github_com-prometheus-common-model.go | 142 +++++++++ ...com-prometheus-prometheus-model-rulefmt.go | 25 ++ ...com-prometheus-prometheus-promql-parser.go | 289 ++++++++++++++++++ ...b_com-slok-sloth-pkg-common-conventions.go | 28 ++ .../github_com-slok-sloth-pkg-common-model.go | 41 +++ ...ub_com-slok-sloth-pkg-common-utils-data.go | 15 + ...-slok-sloth-pkg-common-utils-prometheus.go | 16 + ...slok-sloth-pkg-prometheus-plugin-slo-v1.go | 43 +++ internal/plugin/slo/slo.go | 123 ++++++++ internal/plugin/slo/slo_test.go | 176 +++++++++++ internal/prometheus/model.go | 14 - internal/storage/io/k8s_sloth.go | 19 +- internal/storage/io/k8s_sloth_test.go | 29 +- internal/storage/io/openslo_test.go | 21 +- .../storage/io/prometheus_operator_test.go | 21 +- internal/storage/io/sloth_test.go | 41 ++- internal/storage/k8s/k8s_test.go | 9 +- pkg/common/utils/data/data.go | 14 +- .../plugin/slo/v1/testing/testing.go | 56 ++++ pkg/prometheus/plugin/slo/v1/v1.go | 54 ++++ 38 files changed, 2036 insertions(+), 620 deletions(-) rename internal/app/generate/{prometheus.go => generate.go} (55%) rename internal/app/generate/{prometheus_test.go => generate_test.go} (100%) delete mode 100644 internal/app/generate/noop.go delete mode 100644 internal/app/generate/process_core.go rename internal/{prometheus/alert_rules.go => plugin/slo/core/alert_rules_v1/plugin.go} (78%) rename internal/{prometheus/alert_rules_test.go => plugin/slo/core/alert_rules_v1/plugin_test.go} (72%) create mode 100644 internal/plugin/slo/core/metadata_rules_v1/plugin.go create mode 100644 internal/plugin/slo/core/metadata_rules_v1/plugin_test.go create mode 100644 internal/plugin/slo/core/noop_v1/plugin.go create mode 100644 internal/plugin/slo/core/noop_v1/plugin_test.go rename internal/{prometheus/recording_rules.go => plugin/slo/core/sli_rules_v1/plugin.go} (56%) rename internal/{prometheus/recording_rules_test.go => plugin/slo/core/sli_rules_v1/plugin_test.go} (70%) create mode 100644 internal/plugin/slo/custom/custom.go create mode 100644 internal/plugin/slo/custom/github_com-prometheus-common-model.go create mode 100644 internal/plugin/slo/custom/github_com-prometheus-prometheus-model-rulefmt.go create mode 100644 internal/plugin/slo/custom/github_com-prometheus-prometheus-promql-parser.go create mode 100644 internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-conventions.go create mode 100644 internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-model.go create mode 100644 internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go create mode 100644 internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go create mode 100644 internal/plugin/slo/custom/github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go create mode 100644 internal/plugin/slo/slo.go create mode 100644 internal/plugin/slo/slo_test.go delete mode 100644 internal/prometheus/model.go create mode 100644 pkg/prometheus/plugin/slo/v1/testing/testing.go create mode 100644 pkg/prometheus/plugin/slo/v1/v1.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 002708aa..425b261d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Sloth domain models can be imported in Go apps using `github.com/slok/sloth/pkg/common/model`. - Sloth conventions can be imported in Go apps using `github.com/slok/sloth/pkg/common/conventions`. +- A new SLO rule generation plugin system has been added to be able to change/extend the SLO rule generation process. ## [v0.12.0] - 2025-03-27 diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index a1b75c4a..c5f6f4c4 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -20,7 +20,9 @@ import ( "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/prometheus" + plugincorealertrulesv1 "github.com/slok/sloth/internal/plugin/slo/core/alert_rules_v1" + plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" + plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" "github.com/slok/sloth/internal/storage" storageio "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" @@ -309,7 +311,7 @@ type generator struct { } // GeneratePrometheus generates the SLOs based on a raw regular Prometheus spec format input and outs a Prometheus raw yaml. -func (g generator) GeneratePrometheus(ctx context.Context, slos prometheus.SLOGroup, out io.Writer) error { +func (g generator) GeneratePrometheus(ctx context.Context, slos model.PromSLOGroup, out io.Writer) error { g.logger.Infof("Generating from Prometheus spec") info := model.Info{ Version: info.Version, @@ -381,7 +383,7 @@ func (g generator) GenerateKubernetes(ctx context.Context, sloGroup model.PromSL } // generateOpenSLO generates the SLOs based on a OpenSLO spec format input and outs a Prometheus raw yaml. -func (g generator) GenerateOpenSLO(ctx context.Context, slos prometheus.SLOGroup, out io.Writer) error { +func (g generator) GenerateOpenSLO(ctx context.Context, slos model.PromSLOGroup, out io.Writer) error { g.logger.Infof("Generating from OpenSLO spec") info := model.Info{ Version: info.Version, @@ -412,32 +414,53 @@ func (g generator) GenerateOpenSLO(ctx context.Context, slos prometheus.SLOGroup } // generate is the main generator logic that all the spec types and storers share. Mainly has the logic of the generate app service. -func (g generator) generateRules(ctx context.Context, info model.Info, slos prometheus.SLOGroup) (*generate.Response, error) { +func (g generator) generateRules(ctx context.Context, info model.Info, slos model.PromSLOGroup) (*generate.Response, error) { // Disable recording rules if required. - var sliRuleGen generate.SLIRecordingRulesGenerator = generate.NoopSLIRecordingRulesGenerator - var metaRuleGen generate.MetadataRecordingRulesGenerator = generate.NoopMetadataRecordingRulesGenerator + var sliRuleGen generate.SLOProcessor = generate.NoopPlugin + var metaRuleGen generate.SLOProcessor = generate.NoopPlugin if !g.disableRecordings { - // Disable optimized rules if required. - sliRuleGen = prometheus.OptimizedSLIRecordingRulesGenerator - if g.disableOptimizedRules { - sliRuleGen = prometheus.SLIRecordingRulesGenerator + sliPlugin, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincoreslirulesv1.NewPlugin, + g.logger.WithValues(log.Kv{"plugin": plugincoreslirulesv1.PluginID}), + plugincoreslirulesv1.PluginConfig{Optimized: !g.disableOptimizedRules}, + ) + if err != nil { + return nil, fmt.Errorf("could not create SLI rules plugin: %w", err) + } + sliRuleGen = sliPlugin + + metadataPlugin, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincoremetadatarulesv1.NewPlugin, + g.logger.WithValues(log.Kv{"plugin": plugincoremetadatarulesv1.PluginID}), + nil, + ) + if err != nil { + return nil, fmt.Errorf("could not create metadata rules plugin: %w", err) } - metaRuleGen = prometheus.MetadataRecordingRulesGenerator + metaRuleGen = metadataPlugin } // Disable alert rules if required. - var alertRuleGen generate.SLOAlertRulesGenerator = generate.NoopSLOAlertRulesGenerator + var alertRuleGen generate.SLOProcessor = generate.NoopPlugin if !g.disableAlerts { - alertRuleGen = prometheus.SLOAlertRulesGenerator + plugin, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincorealertrulesv1.NewPlugin, + g.logger.WithValues(log.Kv{"plugin": plugincorealertrulesv1.PluginID}), + nil, + ) + if err != nil { + return nil, fmt.Errorf("could not create alert rules plugin: %w", err) + } + alertRuleGen = plugin } // Generate. controller, err := generate.NewService(generate.ServiceConfig{ - AlertGenerator: alert.NewGenerator(g.windowsRepo), - SLIRecordingRulesGenerator: sliRuleGen, - MetaRecordingRulesGenerator: metaRuleGen, - SLOAlertRulesGenerator: alertRuleGen, - Logger: g.logger, + AlertGenerator: alert.NewGenerator(g.windowsRepo), + SLIRulesGenSLOPlugin: sliRuleGen, + MetadataRulesGenSLOPlugin: metaRuleGen, + AlertRulesGenSLOPlugin: alertRuleGen, + Logger: g.logger, }) if err != nil { return nil, fmt.Errorf("could not create application service: %w", err) diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index 305a4be6..fc6fbe30 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -33,7 +33,9 @@ import ( "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/app/kubecontroller" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/prometheus" + plugincorealertrulesv1 "github.com/slok/sloth/internal/plugin/slo/core/alert_rules_v1" + plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" + plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" "github.com/slok/sloth/internal/storage" storageio "github.com/slok/sloth/internal/storage/io" storagek8s "github.com/slok/sloth/internal/storage/k8s" @@ -299,26 +301,47 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error ctx, cancel := context.WithCancel(ctx) defer cancel() - // Disable optimized rules. - sliRuleGen := prometheus.OptimizedSLIRecordingRulesGenerator - if k.disableOptimizedRules { - sliRuleGen = prometheus.SLIRecordingRulesGenerator + sliRuleGen, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincoreslirulesv1.NewPlugin, + logger.WithValues(log.Kv{"plugin": plugincoreslirulesv1.PluginID}), + plugincoreslirulesv1.PluginConfig{Optimized: !k.disableOptimizedRules}, + ) + if err != nil { + return fmt.Errorf("could not create SLI rules plugin: %w", err) + } + + metaRuleGen, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincoremetadatarulesv1.NewPlugin, + logger.WithValues(log.Kv{"plugin": plugincoremetadatarulesv1.PluginID}), + nil, + ) + if err != nil { + return fmt.Errorf("could not create metadata rules plugin: %w", err) + } + + alertRuleGen, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincorealertrulesv1.NewPlugin, + logger.WithValues(log.Kv{"plugin": plugincorealertrulesv1.PluginID}), + nil, + ) + if err != nil { + return fmt.Errorf("could not create alert rules plugin: %w", err) } // Create the generate app service (the one that the CLIs use). generator, err := generate.NewService(generate.ServiceConfig{ - AlertGenerator: alert.NewGenerator(windowsRepo), - SLIRecordingRulesGenerator: sliRuleGen, - MetaRecordingRulesGenerator: prometheus.MetadataRecordingRulesGenerator, - SLOAlertRulesGenerator: prometheus.SLOAlertRulesGenerator, - Logger: generatorLogger{Logger: logger}, + AlertGenerator: alert.NewGenerator(windowsRepo), + SLIRulesGenSLOPlugin: sliRuleGen, + MetadataRulesGenSLOPlugin: metaRuleGen, + AlertRulesGenSLOPlugin: alertRuleGen, + Logger: generatorLogger{Logger: logger}, }) if err != nil { return fmt.Errorf("could not create Prometheus rules generator: %w", err) } // Create handler. - config := kubecontroller.HandlerConfig{ + controllerConfig := kubecontroller.HandlerConfig{ Generator: generator, SpecLoader: storageio.NewK8sSlothPrometheusCRSpecLoader(pluginRepo, sloPeriod), Repository: kuberepo, @@ -326,7 +349,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error ExtraLabels: k.extraLabels, Logger: logger, } - handler, err := kubecontroller.NewHandler(config) + handler, err := kubecontroller.NewHandler(controllerConfig) if err != nil { return fmt.Errorf("could not create controller handler: %w", err) } diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index a1189c91..7cde5fdc 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -6,6 +6,7 @@ ARG GOLANGCI_LINT_VERSION="1.64.8" ARG MOCKERY_VERSION="2.53.3" ARG GOMARKDOC_VERSION="1.1.0" ARG HELM_VERSION="3.17.0" +ARG YAEGI_VERSION="0.16.1" ARG ostype=Linux RUN apt-get update && apt-get install -y \ @@ -31,7 +32,12 @@ RUN wget https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCI wget https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz && \ tar zxvf helm-v${HELM_VERSION}-linux-amd64.tar.gz -C /tmp && \ mv /tmp/linux-amd64/helm /usr/local/bin/ && \ - rm -rf helm-v${HELM_VERSION}-linux-amd64.tar.gz /tmp/linux-amd64 + rm -rf helm-v${HELM_VERSION}-linux-amd64.tar.gz /tmp/linux-amd64 && \ + \ + wget https://github.com/traefik/yaegi/releases/download/v${YAEGI_VERSION}/yaegi_v${YAEGI_VERSION}_linux_amd64.tar.gz && \ + tar zxvf yaegi_v${YAEGI_VERSION}_linux_amd64.tar.gz -C /tmp && \ + mv /tmp/yaegi /usr/local/bin/ && \ + rm yaegi_v${YAEGI_VERSION}_linux_amd64.tar.gz # Create user. diff --git a/internal/app/generate/prometheus.go b/internal/app/generate/generate.go similarity index 55% rename from internal/app/generate/prometheus.go rename to internal/app/generate/generate.go index 60d54509..093d2efc 100644 --- a/internal/app/generate/prometheus.go +++ b/internal/app/generate/generate.go @@ -4,22 +4,28 @@ import ( "context" "fmt" - "github.com/prometheus/prometheus/model/rulefmt" - "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/prometheus" + plugincorealertrulesv1 "github.com/slok/sloth/internal/plugin/slo/core/alert_rules_v1" + plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" + plugincorenoopsv1 "github.com/slok/sloth/internal/plugin/slo/core/noop_v1" + plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" ) +// Default plugins. +var ( + NoopPlugin, _ = NewSLOProcessorFromSLOPluginV1(plugincorenoopsv1.NewPlugin, log.Noop, nil) +) + // ServiceConfig is the application service configuration. type ServiceConfig struct { - AlertGenerator AlertGenerator - SLIRecordingRulesGenerator SLIRecordingRulesGenerator - MetaRecordingRulesGenerator MetadataRecordingRulesGenerator - SLOAlertRulesGenerator SLOAlertRulesGenerator - Logger log.Logger + AlertGenerator AlertGenerator + SLIRulesGenSLOPlugin SLOProcessor + AlertRulesGenSLOPlugin SLOProcessor + MetadataRulesGenSLOPlugin SLOProcessor + Logger log.Logger } func (c *ServiceConfig) defaults() error { @@ -27,22 +33,45 @@ func (c *ServiceConfig) defaults() error { return fmt.Errorf("alert generator is required") } - if c.SLIRecordingRulesGenerator == nil { - c.SLIRecordingRulesGenerator = prometheus.OptimizedSLIRecordingRulesGenerator + if c.Logger == nil { + c.Logger = log.Noop } + c.Logger = c.Logger.WithValues(log.Kv{"svc": "generate.prometheus.Service"}) - if c.MetaRecordingRulesGenerator == nil { - c.MetaRecordingRulesGenerator = prometheus.MetadataRecordingRulesGenerator + // Default plugins. + if c.SLIRulesGenSLOPlugin == nil { + plugin, err := NewSLOProcessorFromSLOPluginV1( + plugincoreslirulesv1.NewPlugin, + c.Logger.WithValues(log.Kv{"plugin": plugincoreslirulesv1.PluginID}), + plugincoreslirulesv1.PluginConfig{Optimized: true}, + ) + if err != nil { + return fmt.Errorf("could not create SLI rules plugin: %w", err) + } + c.SLIRulesGenSLOPlugin = plugin } - - if c.SLOAlertRulesGenerator == nil { - c.SLOAlertRulesGenerator = prometheus.SLOAlertRulesGenerator + if c.AlertRulesGenSLOPlugin == nil { + plugin, err := NewSLOProcessorFromSLOPluginV1( + plugincorealertrulesv1.NewPlugin, + c.Logger.WithValues(log.Kv{"plugin": plugincorealertrulesv1.PluginID}), + nil, + ) + if err != nil { + return fmt.Errorf("could not create alert rules plugin: %w", err) + } + c.AlertRulesGenSLOPlugin = plugin } - - if c.Logger == nil { - c.Logger = log.Noop + if c.MetadataRulesGenSLOPlugin == nil { + plugin, err := NewSLOProcessorFromSLOPluginV1( + plugincoremetadatarulesv1.NewPlugin, + c.Logger.WithValues(log.Kv{"plugin": plugincoremetadatarulesv1.PluginID}), + nil, + ) + if err != nil { + return fmt.Errorf("could not create metadata rules plugin: %w", err) + } + c.MetadataRulesGenSLOPlugin = plugin } - c.Logger = c.Logger.WithValues(log.Kv{"svc": "generate.prometheus.Service"}) return nil } @@ -52,28 +81,11 @@ type AlertGenerator interface { GenerateMWMBAlerts(ctx context.Context, slo alert.SLO) (*model.MWMBAlertGroup, error) } -// SLIRecordingRulesGenerator knows how to generate SLI recording rules. -type SLIRecordingRulesGenerator interface { - GenerateSLIRecordingRules(ctx context.Context, slo prometheus.SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) -} - -// MetadataRecordingRulesGenerator knows how to generate metadata recording rules. -type MetadataRecordingRulesGenerator interface { - GenerateMetadataRecordingRules(ctx context.Context, info model.Info, slo prometheus.SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) -} - -// SLOAlertRulesGenerator knows hot to generate SLO alert rules. -type SLOAlertRulesGenerator interface { - GenerateSLOAlertRules(ctx context.Context, slo prometheus.SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) -} - // Service is the application service for the generation of SLO for Prometheus. type Service struct { - alertGen AlertGenerator - sliRecordRuleGen SLIRecordingRulesGenerator - metaRecordRuleGen MetadataRecordingRulesGenerator - alertRuleGen SLOAlertRulesGenerator - logger log.Logger + alertGen AlertGenerator + defaultPlugins []SLOProcessor + logger log.Logger } // NewService returns a new Prometheus application service. @@ -84,11 +96,13 @@ func NewService(config ServiceConfig) (*Service, error) { } return &Service{ - alertGen: config.AlertGenerator, - sliRecordRuleGen: config.SLIRecordingRulesGenerator, - metaRecordRuleGen: config.MetaRecordingRulesGenerator, - alertRuleGen: config.SLOAlertRulesGenerator, - logger: config.Logger, + alertGen: config.AlertGenerator, + defaultPlugins: []SLOProcessor{ + config.SLIRulesGenSLOPlugin, + config.AlertRulesGenSLOPlugin, + config.MetadataRulesGenSLOPlugin, + }, + logger: config.Logger, }, nil } @@ -136,7 +150,7 @@ func (s Service) Generate(ctx context.Context, r Request) (*Response, error) { }, nil } -func (s Service) generateSLO(ctx context.Context, info model.Info, slo prometheus.SLO) (*model.PromSLORules, error) { +func (s Service) generateSLO(ctx context.Context, info model.Info, slo model.PromSLO) (*model.PromSLORules, error) { logger := s.logger.WithCtxValues(ctx).WithValues(log.Kv{"slo": slo.ID}) // Generate the MWMB alerts. @@ -151,20 +165,15 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, slo prometheu } logger.Debugf("Multiwindow-multiburn alerts generated") - // Set default processors. - processors := []sloProcessor{ - newProcessorForMetaRecordingRulesGenerator(logger, s.metaRecordRuleGen), - newProcessorForSLIRecordingRulesGenerator(logger, s.sliRecordRuleGen), - newProcessorForSLOAlertRulesGenerator(logger, s.alertRuleGen), - } - - req := &sloProcessortRequest{ - Info: info, - Alerts: *as, - SLO: slo, + // Generate plugins. + sloProcessors := s.defaultPlugins + req := &SLOProcessorRequest{ + Info: info, + MWMBAlertGroup: *as, + SLO: slo, } - res := &sloProcessortResult{} - for _, p := range processors { + res := &SLOProcessorResult{} + for _, p := range sloProcessors { err := p.ProcessSLO(ctx, req, res) if err != nil { return nil, fmt.Errorf("slo processor failed: %w", err) diff --git a/internal/app/generate/prometheus_test.go b/internal/app/generate/generate_test.go similarity index 100% rename from internal/app/generate/prometheus_test.go rename to internal/app/generate/generate_test.go diff --git a/internal/app/generate/noop.go b/internal/app/generate/noop.go deleted file mode 100644 index a9d90c39..00000000 --- a/internal/app/generate/noop.go +++ /dev/null @@ -1,32 +0,0 @@ -package generate - -import ( - "context" - - "github.com/prometheus/prometheus/model/rulefmt" - "github.com/slok/sloth/pkg/common/model" -) - -type noopSLIRecordingRulesGenerator bool - -const NoopSLIRecordingRulesGenerator = noopSLIRecordingRulesGenerator(false) - -func (noopSLIRecordingRulesGenerator) GenerateSLIRecordingRules(ctx context.Context, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { - return nil, nil -} - -type noopMetadataRecordingRulesGenerator bool - -const NoopMetadataRecordingRulesGenerator = noopMetadataRecordingRulesGenerator(false) - -func (noopMetadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx context.Context, info model.Info, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { - return nil, nil -} - -type noopSLOAlertRulesGenerator bool - -const NoopSLOAlertRulesGenerator = noopSLOAlertRulesGenerator(false) - -func (noopSLOAlertRulesGenerator) GenerateSLOAlertRules(ctx context.Context, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { - return nil, nil -} diff --git a/internal/app/generate/process.go b/internal/app/generate/process.go index d9e311f7..0dba86ea 100644 --- a/internal/app/generate/process.go +++ b/internal/app/generate/process.go @@ -2,26 +2,72 @@ package generate import ( "context" + "encoding/json" + "fmt" + "github.com/slok/sloth/internal/log" "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" ) -type sloProcessortRequest struct { - Info model.Info - SLO model.PromSLO - Alerts model.MWMBAlertGroup +type SLOProcessorRequest struct { + Info model.Info + SLO model.PromSLO + MWMBAlertGroup model.MWMBAlertGroup } -type sloProcessortResult struct { +type SLOProcessorResult struct { SLORules model.PromSLORules } -type sloProcessor interface { - ProcessSLO(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error +// SLOProcessor is the interface that will be used to process SLO and generate rules. +// This is an abstraction to be able to support multiple and different SLO plugin versions +// or custom internal processors. +type SLOProcessor interface { + ProcessSLO(ctx context.Context, req *SLOProcessorRequest, res *SLOProcessorResult) error } -// sloProcessorFunc is a helper function to create processors easily. -type sloProcessorFunc func(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error +// SLOProcessorFunc is a helper function to create processors easily. +type SLOProcessorFunc func(ctx context.Context, req *SLOProcessorRequest, res *SLOProcessorResult) error -func (s sloProcessorFunc) ProcessSLO(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error { +func (s SLOProcessorFunc) ProcessSLO(ctx context.Context, req *SLOProcessorRequest, res *SLOProcessorResult) error { return s(ctx, req, res) } + +// NewSLOProcessorFromSLOPluginV1 will be able to map a SLO plugin v1 to the SLOProcessor interface. +func NewSLOProcessorFromSLOPluginV1(pluginFactory pluginslov1.PluginFactory, logger log.Logger, config any) (SLOProcessor, error) { + configData, err := json.Marshal(config) + if err != nil { + return nil, fmt.Errorf("could not marshal config: %w", err) + } + + plugin, err := pluginFactory(configData, pluginslov1.AppUtils{Logger: logger}) + if err != nil { + return nil, fmt.Errorf("could not create plugin: %w", err) + } + + return SLOProcessorFunc(func(ctx context.Context, req *SLOProcessorRequest, res *SLOProcessorResult) error { + // Map models for slo plugin V1 version. + r := &pluginslov1.Request{ + Info: req.Info, + SLO: req.SLO, + MWMBAlertGroup: req.MWMBAlertGroup, + } + rs := &pluginslov1.Result{ + SLORules: res.SLORules, + } + + // Process plugin. + err := plugin.ProcessSLO(ctx, r, rs) + if err != nil { + return err + } + + // Unmap models for slo plugin V1 version. + req.Info = r.Info + req.SLO = r.SLO + req.MWMBAlertGroup = r.MWMBAlertGroup + res.SLORules = rs.SLORules + + return nil + }), nil +} diff --git a/internal/app/generate/process_core.go b/internal/app/generate/process_core.go deleted file mode 100644 index 2268bafc..00000000 --- a/internal/app/generate/process_core.go +++ /dev/null @@ -1,47 +0,0 @@ -package generate - -import ( - "context" - "fmt" - - "github.com/slok/sloth/internal/log" -) - -func newProcessorForSLIRecordingRulesGenerator(logger log.Logger, gen SLIRecordingRulesGenerator) sloProcessor { - return sloProcessorFunc(func(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error { - rules, err := gen.GenerateSLIRecordingRules(ctx, req.SLO, req.Alerts) - if err != nil { - return fmt.Errorf("could not generate Prometheus sli recording rules: %w", err) - } - res.SLORules.SLIErrorRecRules.Rules = rules - - logger.WithValues(log.Kv{"rules": len(rules)}).Infof("SLI recording rules generated") - return nil - }) -} - -func newProcessorForMetaRecordingRulesGenerator(logger log.Logger, gen MetadataRecordingRulesGenerator) sloProcessor { - return sloProcessorFunc(func(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error { - rules, err := gen.GenerateMetadataRecordingRules(ctx, req.Info, req.SLO, req.Alerts) - if err != nil { - return fmt.Errorf("could not generate Prometheus metadata recording rules: %w", err) - } - res.SLORules.MetadataRecRules.Rules = rules - - logger.WithValues(log.Kv{"rules": len(rules)}).Infof("Metadata recording rules generated") - return nil - }) -} - -func newProcessorForSLOAlertRulesGenerator(logger log.Logger, gen SLOAlertRulesGenerator) sloProcessor { - return sloProcessorFunc(func(ctx context.Context, req *sloProcessortRequest, res *sloProcessortResult) error { - rules, err := gen.GenerateSLOAlertRules(ctx, req.SLO, req.Alerts) - if err != nil { - return fmt.Errorf("could not generate Prometheus alert rules: %w", err) - } - res.SLORules.AlertRules.Rules = rules - - logger.WithValues(log.Kv{"rules": len(rules)}).Infof("SLO alert rules generated") - return nil - }) -} diff --git a/internal/prometheus/alert_rules.go b/internal/plugin/slo/core/alert_rules_v1/plugin.go similarity index 78% rename from internal/prometheus/alert_rules.go rename to internal/plugin/slo/core/alert_rules_v1/plugin.go index c9b2777b..fbcf531f 100644 --- a/internal/prometheus/alert_rules.go +++ b/internal/plugin/slo/core/alert_rules_v1/plugin.go @@ -1,8 +1,9 @@ -package prometheus +package plugin import ( "bytes" "context" + "encoding/json" "fmt" "text/template" @@ -12,25 +13,37 @@ import ( "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" promutils "github.com/slok/sloth/pkg/common/utils/prometheus" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" ) -// genFunc knows how to generate an SLI recording rule for a specific time window. -type alertGenFunc func(slo SLO, sloAlert AlertMeta, quick, slow model.MWMBAlert) (*rulefmt.Rule, error) +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/core/alert_rules/v1" +) -type sloAlertRulesGenerator struct { - alertGenFunc alertGenFunc +func NewPlugin(_ json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return plugin{}, nil } -// SLOAlertRulesGenerator knows how to generate the SLO prometheus alert rules -// from an SLO. -var SLOAlertRulesGenerator = sloAlertRulesGenerator{alertGenFunc: defaultSLOAlertGenerator} +type plugin struct{} + +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + rules, err := p.generateSLOAlertRules(ctx, request.SLO, request.MWMBAlertGroup) + if err != nil { + return err + } + + result.SLORules.AlertRules.Rules = rules + + return nil +} -func (s sloAlertRulesGenerator) GenerateSLOAlertRules(ctx context.Context, slo SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { +func (p plugin) generateSLOAlertRules(ctx context.Context, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { rules := []rulefmt.Rule{} // Generate Page alerts. if !slo.PageAlertMeta.Disable { - rule, err := s.alertGenFunc(slo, slo.PageAlertMeta, alerts.PageQuick, alerts.PageSlow) + rule, err := defaultSLOAlertGenerator(slo, slo.PageAlertMeta, alerts.PageQuick, alerts.PageSlow) if err != nil { return nil, fmt.Errorf("could not create page alert: %w", err) } @@ -40,7 +53,7 @@ func (s sloAlertRulesGenerator) GenerateSLOAlertRules(ctx context.Context, slo S // Generate Ticket alerts. if !slo.TicketAlertMeta.Disable { - rule, err := s.alertGenFunc(slo, slo.TicketAlertMeta, alerts.TicketQuick, alerts.TicketSlow) + rule, err := defaultSLOAlertGenerator(slo, slo.TicketAlertMeta, alerts.TicketQuick, alerts.TicketSlow) if err != nil { return nil, fmt.Errorf("could not create ticket alert: %w", err) } @@ -51,7 +64,7 @@ func (s sloAlertRulesGenerator) GenerateSLOAlertRules(ctx context.Context, slo S return rules, nil } -func defaultSLOAlertGenerator(slo SLO, sloAlert AlertMeta, quick, slow model.MWMBAlert) (*rulefmt.Rule, error) { +func defaultSLOAlertGenerator(slo model.PromSLO, sloAlert model.PromAlertMeta, quick, slow model.MWMBAlert) (*rulefmt.Rule, error) { // Generate the filter labels based on the SLO ids. metricFilter := promutils.LabelsToPromFilter(conventions.GetSLOIDPromLabels(slo)) diff --git a/internal/prometheus/alert_rules_test.go b/internal/plugin/slo/core/alert_rules_v1/plugin_test.go similarity index 72% rename from internal/prometheus/alert_rules_test.go rename to internal/plugin/slo/core/alert_rules_v1/plugin_test.go index a39648a0..da6eb73d 100644 --- a/internal/prometheus/alert_rules_test.go +++ b/internal/plugin/slo/core/alert_rules_v1/plugin_test.go @@ -1,18 +1,20 @@ -package prometheus_test +package plugin_test import ( - "context" "testing" "time" "github.com/prometheus/prometheus/model/rulefmt" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - "github.com/slok/sloth/internal/prometheus" + plugin "github.com/slok/sloth/internal/plugin/slo/core/alert_rules_v1" "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" ) -func getSLOAlertGroup() model.MWMBAlertGroup { +func baseSLOAlertGroup() model.MWMBAlertGroup { return model.MWMBAlertGroup{ PageQuick: model.MWMBAlert{ ID: "10", @@ -49,30 +51,34 @@ func getSLOAlertGroup() model.MWMBAlertGroup { } } -func TestGenerateSLOAlertRules(t *testing.T) { +func baseSLO() model.PromSLO { + return model.PromSLO{ + ID: "test-svc-test", + Name: "test", + Service: "test-svc", + PageAlertMeta: model.PromAlertMeta{ + Name: "something1", + Labels: map[string]string{"custom-label": "test1"}, + Annotations: map[string]string{"custom-annot": "test1"}, + }, + TicketAlertMeta: model.PromAlertMeta{ + Name: "something2", + Labels: map[string]string{"custom-label": "test2"}, + Annotations: map[string]string{"custom-annot": "test2"}, + }, + } +} + +func TestPlugin(t *testing.T) { tests := map[string]struct { - slo prometheus.SLO + slo model.PromSLO alertGroup func() model.MWMBAlertGroup expRules []rulefmt.Rule expErr bool }{ "Having and SLO an its page and ticket alerts should create the recording rules.": { - slo: prometheus.SLO{ - ID: "test-svc-test", - Name: "test", - Service: "test-svc", - PageAlertMeta: prometheus.AlertMeta{ - Name: "something1", - Labels: map[string]string{"custom-label": "test1"}, - Annotations: map[string]string{"custom-annot": "test1"}, - }, - TicketAlertMeta: prometheus.AlertMeta{ - Name: "something2", - Labels: map[string]string{"custom-label": "test2"}, - Annotations: map[string]string{"custom-annot": "test2"}, - }, - }, - alertGroup: getSLOAlertGroup, + slo: baseSLO(), + alertGroup: baseSLOAlertGroup, expRules: []rulefmt.Rule{ { Alert: "something1", @@ -126,20 +132,20 @@ or }, "Having and SLO an page and disabled ticket alerts should only create only page alert rules.": { - slo: prometheus.SLO{ + slo: model.PromSLO{ ID: "test-svc-test", Name: "test", Service: "test-svc", - PageAlertMeta: prometheus.AlertMeta{ + PageAlertMeta: model.PromAlertMeta{ Name: "something1", Labels: map[string]string{"custom-label": "test1"}, Annotations: map[string]string{"custom-annot": "test1"}, }, - TicketAlertMeta: prometheus.AlertMeta{ + TicketAlertMeta: model.PromAlertMeta{ Disable: true, }, }, - alertGroup: getSLOAlertGroup, + alertGroup: baseSLOAlertGroup, expRules: []rulefmt.Rule{ { Alert: "something1", @@ -168,20 +174,20 @@ or }, }, "Having and SLO an ticker and page alerts disabled should only create ticket alert rules.": { - slo: prometheus.SLO{ + slo: model.PromSLO{ ID: "test-svc-test", Name: "test", Service: "test-svc", - PageAlertMeta: prometheus.AlertMeta{ + PageAlertMeta: model.PromAlertMeta{ Disable: true, }, - TicketAlertMeta: prometheus.AlertMeta{ + TicketAlertMeta: model.PromAlertMeta{ Name: "something2", Labels: map[string]string{"custom-label": "test2"}, Annotations: map[string]string{"custom-annot": "test2"}, }, }, - alertGroup: getSLOAlertGroup, + alertGroup: baseSLOAlertGroup, expRules: []rulefmt.Rule{ { Alert: "something2", @@ -214,14 +220,62 @@ or for name, test := range tests { t.Run(name, func(t *testing.T) { assert := assert.New(t) + require := require.New(t) + + // Load plugin + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{}) + require.NoError(err) - gotRules, err := prometheus.SLOAlertRulesGenerator.GenerateSLOAlertRules(context.TODO(), test.slo, test.alertGroup()) + // Execute plugin. + req := pluginslov1.Request{ + SLO: test.slo, + MWMBAlertGroup: test.alertGroup(), + } + res := pluginslov1.Result{} + err = plugin.ProcessSLO(t.Context(), &req, &res) + // Check result. if test.expErr { assert.Error(err) } else if assert.NoError(err) { - assert.Equal(test.expRules, gotRules) + assert.Equal(test.expRules, res.SLORules.AlertRules.Rules) } }) } } + +func BenchmarkPluginYaegi(b *testing.B) { + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{}) + if err != nil { + b.Fatal(err) + } + + req := &pluginslov1.Request{ + SLO: baseSLO(), + MWMBAlertGroup: baseSLOAlertGroup(), + } + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), req, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPluginGo(b *testing.B) { + plugin, err := plugin.NewPlugin(nil, pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } + + req := &pluginslov1.Request{ + SLO: baseSLO(), + MWMBAlertGroup: baseSLOAlertGroup(), + } + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), req, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/internal/plugin/slo/core/metadata_rules_v1/plugin.go b/internal/plugin/slo/core/metadata_rules_v1/plugin.go new file mode 100644 index 00000000..65287de9 --- /dev/null +++ b/internal/plugin/slo/core/metadata_rules_v1/plugin.go @@ -0,0 +1,145 @@ +package plugin + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strconv" + "text/template" + + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/slok/sloth/pkg/common/conventions" + "github.com/slok/sloth/pkg/common/model" + utilsdata "github.com/slok/sloth/pkg/common/utils/data" + promutils "github.com/slok/sloth/pkg/common/utils/prometheus" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/core/metadata_rules/v1" +) + +func NewPlugin(_ json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return plugin{}, nil +} + +type plugin struct{} + +func (plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + metadataRules, err := generateMetadataRecordingRules(ctx, request.Info, request.SLO, request.MWMBAlertGroup) + if err != nil { + return err + } + result.SLORules.MetadataRecRules.Rules = metadataRules + return nil +} + +func generateMetadataRecordingRules(ctx context.Context, info model.Info, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { + labels := utilsdata.MergeLabels(conventions.GetSLOIDPromLabels(slo), slo.Labels) + + // Metatada Recordings. + const ( + metricSLOObjectiveRatio = "slo:objective:ratio" + metricSLOErrorBudgetRatio = "slo:error_budget:ratio" + metricSLOTimePeriodDays = "slo:time_period:days" + metricSLOCurrentBurnRateRatio = "slo:current_burn_rate:ratio" + metricSLOPeriodBurnRateRatio = "slo:period_burn_rate:ratio" + metricSLOPeriodErrorBudgetRemainingRatio = "slo:period_error_budget_remaining:ratio" + metricSLOInfo = "sloth_slo_info" + ) + + sloObjectiveRatio := slo.Objective / 100 + + sloFilter := promutils.LabelsToPromFilter(conventions.GetSLOIDPromLabels(slo)) + + var currentBurnRateExpr bytes.Buffer + err := burnRateRecordingExprTpl.Execute(¤tBurnRateExpr, map[string]string{ + "SLIErrorMetric": conventions.GetSLIErrorMetric(alerts.PageQuick.ShortWindow), + "MetricFilter": sloFilter, + "SLOIDName": conventions.PromSLOIDLabelName, + "SLOLabelName": conventions.PromSLONameLabelName, + "SLOServiceName": conventions.PromSLOServiceLabelName, + "ErrorBudgetRatioMetric": metricSLOErrorBudgetRatio, + }) + if err != nil { + return nil, fmt.Errorf("could not render current burn rate prometheus metadata recording rule expression: %w", err) + } + + var periodBurnRateExpr bytes.Buffer + err = burnRateRecordingExprTpl.Execute(&periodBurnRateExpr, map[string]string{ + "SLIErrorMetric": conventions.GetSLIErrorMetric(slo.TimeWindow), + "MetricFilter": sloFilter, + "SLOIDName": conventions.PromSLOIDLabelName, + "SLOLabelName": conventions.PromSLONameLabelName, + "SLOServiceName": conventions.PromSLOServiceLabelName, + "ErrorBudgetRatioMetric": metricSLOErrorBudgetRatio, + }) + if err != nil { + return nil, fmt.Errorf("could not render period burn rate prometheus metadata recording rule expression: %w", err) + } + + rules := []rulefmt.Rule{ + // SLO Objective. + { + Record: metricSLOObjectiveRatio, + Expr: fmt.Sprintf(`vector(%g)`, sloObjectiveRatio), + Labels: labels, + }, + + // Error budget. + { + Record: metricSLOErrorBudgetRatio, + Expr: fmt.Sprintf(`vector(1-%g)`, sloObjectiveRatio), + Labels: labels, + }, + + // Total period. + { + Record: metricSLOTimePeriodDays, + Expr: fmt.Sprintf(`vector(%g)`, slo.TimeWindow.Hours()/24), + Labels: labels, + }, + + // Current burning speed. + { + Record: metricSLOCurrentBurnRateRatio, + Expr: currentBurnRateExpr.String(), + Labels: labels, + }, + + // Total period burn rate. + { + Record: metricSLOPeriodBurnRateRatio, + Expr: periodBurnRateExpr.String(), + Labels: labels, + }, + + // Total Error budget remaining period. + { + Record: metricSLOPeriodErrorBudgetRemainingRatio, + Expr: fmt.Sprintf(`1 - %s%s`, metricSLOPeriodBurnRateRatio, sloFilter), + Labels: labels, + }, + + // Info. + { + Record: metricSLOInfo, + Expr: `vector(1)`, + Labels: utilsdata.MergeLabels(labels, map[string]string{ + conventions.PromSLOVersionLabelName: info.Version, + conventions.PromSLOModeLabelName: string(info.Mode), + conventions.PromSLOSpecLabelName: info.Spec, + conventions.PromSLOObjectiveLabelName: strconv.FormatFloat(slo.Objective, 'f', -1, 64), + }), + }, + } + + return rules, nil +} + +var burnRateRecordingExprTpl = template.Must(template.New("burnRateExpr").Option("missingkey=error").Parse(`{{ .SLIErrorMetric }}{{ .MetricFilter }} +/ on({{ .SLOIDName }}, {{ .SLOLabelName }}, {{ .SLOServiceName }}) group_left +{{ .ErrorBudgetRatioMetric }}{{ .MetricFilter }} +`)) diff --git a/internal/plugin/slo/core/metadata_rules_v1/plugin_test.go b/internal/plugin/slo/core/metadata_rules_v1/plugin_test.go new file mode 100644 index 00000000..72af6234 --- /dev/null +++ b/internal/plugin/slo/core/metadata_rules_v1/plugin_test.go @@ -0,0 +1,220 @@ +package plugin_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/prometheus/prometheus/model/rulefmt" + plugin "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" + "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" +) + +func baseAlertGroup() model.MWMBAlertGroup { + return model.MWMBAlertGroup{ + PageQuick: model.MWMBAlert{ + ShortWindow: 5 * time.Minute, + LongWindow: 1 * time.Hour, + }, + PageSlow: model.MWMBAlert{ + ShortWindow: 30 * time.Minute, + LongWindow: 6 * time.Hour, + }, + TicketQuick: model.MWMBAlert{ + ShortWindow: 2 * time.Hour, + LongWindow: 1 * 24 * time.Hour, + }, + TicketSlow: model.MWMBAlert{ + ShortWindow: 6 * time.Hour, + LongWindow: 3 * 24 * time.Hour, + }, + } +} + +func baseSLO() model.PromSLO { + return model.PromSLO{ + ID: "test", + Name: "test-name", + Service: "test-svc", + Objective: 99.9, + TimeWindow: 30 * 24 * time.Hour, + Labels: map[string]string{ + "kind": "test", + }, + } +} + +func baseInfo() model.Info { + return model.Info{ + Version: "test-ver", + Mode: model.ModeTest, + Spec: "test/v1", + } +} + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + info model.Info + slo model.PromSLO + alertGroup model.MWMBAlertGroup + expRules []rulefmt.Rule + expErr bool + }{ + "Having and SLO an its mwmb alerts should create the metadata recording rules.": { + info: baseInfo(), + slo: baseSLO(), + alertGroup: baseAlertGroup(), + expRules: []rulefmt.Rule{ + { + Record: "slo:objective:ratio", + Expr: "vector(0.9990000000000001)", + Labels: map[string]string{ + "kind": "test", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test", + }, + }, + { + Record: "slo:error_budget:ratio", + Expr: "vector(1-0.9990000000000001)", + Labels: map[string]string{ + "kind": "test", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test", + }, + }, + { + Record: "slo:time_period:days", + Expr: "vector(30)", + Labels: map[string]string{ + "kind": "test", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test", + }, + }, + { + Record: "slo:current_burn_rate:ratio", + Expr: `slo:sli_error:ratio_rate5m{sloth_id="test", sloth_service="test-svc", sloth_slo="test-name"} +/ on(sloth_id, sloth_slo, sloth_service) group_left +slo:error_budget:ratio{sloth_id="test", sloth_service="test-svc", sloth_slo="test-name"} +`, + Labels: map[string]string{ + "kind": "test", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test", + }, + }, + { + Record: "slo:period_burn_rate:ratio", + Expr: `slo:sli_error:ratio_rate30d{sloth_id="test", sloth_service="test-svc", sloth_slo="test-name"} +/ on(sloth_id, sloth_slo, sloth_service) group_left +slo:error_budget:ratio{sloth_id="test", sloth_service="test-svc", sloth_slo="test-name"} +`, + Labels: map[string]string{ + "kind": "test", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test", + }, + }, + { + Record: "slo:period_error_budget_remaining:ratio", + Expr: `1 - slo:period_burn_rate:ratio{sloth_id="test", sloth_service="test-svc", sloth_slo="test-name"}`, + Labels: map[string]string{ + "kind": "test", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test", + }, + }, + { + Record: "sloth_slo_info", + Expr: `vector(1)`, + Labels: map[string]string{ + "kind": "test", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test", + "sloth_version": "test-ver", + "sloth_mode": "test", + "sloth_spec": "test/v1", + "sloth_objective": "99.9", + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Load plugin + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{}) + require.NoError(err) + + // Execute plugin. + req := pluginslov1.Request{ + SLO: test.slo, + Info: test.info, + MWMBAlertGroup: test.alertGroup, + } + res := pluginslov1.Result{} + err = plugin.ProcessSLO(t.Context(), &req, &res) + + // Check result. + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRules, res.SLORules.MetadataRecRules.Rules) + } + }) + } +} + +func BenchmarkPluginYaegi(b *testing.B) { + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{}) + if err != nil { + b.Fatal(err) + } + + req := &pluginslov1.Request{ + SLO: baseSLO(), + Info: baseInfo(), + MWMBAlertGroup: baseAlertGroup(), + } + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), req, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPluginGo(b *testing.B) { + plugin, err := plugin.NewPlugin(nil, pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } + + req := &pluginslov1.Request{ + SLO: baseSLO(), + Info: baseInfo(), + MWMBAlertGroup: baseAlertGroup(), + } + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), req, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/internal/plugin/slo/core/noop_v1/plugin.go b/internal/plugin/slo/core/noop_v1/plugin.go new file mode 100644 index 00000000..845fd32f --- /dev/null +++ b/internal/plugin/slo/core/noop_v1/plugin.go @@ -0,0 +1,23 @@ +package plugin + +import ( + "context" + "encoding/json" + + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/core/noop/v1" +) + +func NewPlugin(_ json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return noopPlugin{}, nil +} + +type noopPlugin struct{} + +func (p noopPlugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + return nil +} diff --git a/internal/plugin/slo/core/noop_v1/plugin_test.go b/internal/plugin/slo/core/noop_v1/plugin_test.go new file mode 100644 index 00000000..d2d3fdc8 --- /dev/null +++ b/internal/plugin/slo/core/noop_v1/plugin_test.go @@ -0,0 +1,74 @@ +package plugin_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + plugin "github.com/slok/sloth/internal/plugin/slo/core/noop_v1" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" +) + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + config json.RawMessage + req pluginslov1.Request + expRes pluginslov1.Result + expErr bool + }{ + "A noop plugin should noop.": { + config: json.RawMessage{}, + req: pluginslov1.Request{}, + expRes: pluginslov1.Result{}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: test.config}) + require.NoError(err) + + res := pluginslov1.Result{} + err = plugin.ProcessSLO(t.Context(), &test.req, &res) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, res) + } + }) + } +} + +func BenchmarkPluginYaegi(b *testing.B) { + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{}) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPluginGo(b *testing.B) { + plugin, err := plugin.NewPlugin(nil, pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/internal/prometheus/recording_rules.go b/internal/plugin/slo/core/sli_rules_v1/plugin.go similarity index 56% rename from internal/prometheus/recording_rules.go rename to internal/plugin/slo/core/sli_rules_v1/plugin.go index 515c3b12..cbe68f04 100644 --- a/internal/prometheus/recording_rules.go +++ b/internal/plugin/slo/core/sli_rules_v1/plugin.go @@ -1,49 +1,61 @@ -package prometheus +package plugin import ( "bytes" "context" + "encoding/json" "fmt" "sort" - "strconv" "text/template" "time" "github.com/prometheus/prometheus/model/rulefmt" - "github.com/slok/sloth/pkg/common/conventions" "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" promutils "github.com/slok/sloth/pkg/common/utils/prometheus" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" ) -// sliRulesgenFunc knows how to generate an SLI recording rule for a specific time window. -type sliRulesgenFunc func(slo SLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/core/sli_rules/v1" +) -type sliRecordingRulesGenerator struct { - genFunc sliRulesgenFunc +type PluginConfig struct { + Optimized bool } -// OptimizedSLIRecordingRulesGenerator knows how to generate the SLI prometheus recording rules -// from an SLO optimizing where it can. -// Normally these rules are used by the SLO alerts. -var OptimizedSLIRecordingRulesGenerator = sliRecordingRulesGenerator{genFunc: optimizedFactorySLIRecordGenerator} +func NewPlugin(c json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + cfg := &PluginConfig{} + err := json.Unmarshal(c, cfg) + if err != nil { + return nil, err + } -// SLIRecordingRulesGenerator knows how to generate the SLI prometheus recording rules -// form an SLO. -// Normally these rules are used by the SLO alerts. -var SLIRecordingRulesGenerator = sliRecordingRulesGenerator{genFunc: factorySLIRecordGenerator} + return plugin{cfg: *cfg}, nil +} -func optimizedFactorySLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { - // Optimize the rules that are for the total period time window. - if window == slo.TimeWindow { - return optimizedSLIRecordGenerator(slo, window, alerts.PageQuick.ShortWindow) +type plugin struct { + cfg PluginConfig +} + +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + genFunc := factorySLIRecordGenerator + if p.cfg.Optimized { + genFunc = optimizedFactorySLIRecordGenerator } - return factorySLIRecordGenerator(slo, window, alerts) + sliRules, err := generateSLIRecordingRules(ctx, request.SLO, request.MWMBAlertGroup, genFunc) + if err != nil { + return err + } + result.SLORules.SLIErrorRecRules.Rules = sliRules + + return nil } -func (s sliRecordingRulesGenerator) GenerateSLIRecordingRules(ctx context.Context, slo SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { +func generateSLIRecordingRules(ctx context.Context, slo model.PromSLO, alerts model.MWMBAlertGroup, genFunc sliRulesgenFunc) ([]rulefmt.Rule, error) { // Get the windows we need the recording rules. windows := getAlertGroupWindows(alerts) windows = append(windows, slo.TimeWindow) // Add the total time window as a handy helper. @@ -51,7 +63,7 @@ func (s sliRecordingRulesGenerator) GenerateSLIRecordingRules(ctx context.Contex // Generate the rules rules := make([]rulefmt.Rule, 0, len(windows)) for _, window := range windows { - rule, err := s.genFunc(slo, window, alerts) + rule, err := genFunc(slo, window, alerts) if err != nil { return nil, fmt.Errorf("could not create %q SLO rule for window %s: %w", slo.ID, window, err) } @@ -61,11 +73,29 @@ func (s sliRecordingRulesGenerator) GenerateSLIRecordingRules(ctx context.Contex return rules, nil } +// sliRulesgenFunc knows how to generate an SLI recording rule for a specific time window. +type sliRulesgenFunc func(slo model.PromSLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) + +// OptimizedSLIRecordingRulesGenerator knows how to generate the SLI prometheus recording rules +// from an SLO optimizing where it can. +// Normally these rules are used by the SLO alerts. +func optimizedFactorySLIRecordGenerator(slo model.PromSLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { + // Optimize the rules that are for the total period time window. + if window == slo.TimeWindow { + return optimizedSLIRecordGenerator(slo, window, alerts.PageQuick.ShortWindow) + } + + return factorySLIRecordGenerator(slo, window, alerts) +} + const ( tplKeyWindow = "window" ) -func factorySLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { +// factorySLIRecordGenerator knows how to generate the SLI prometheus recording rules +// form an SLO. +// Normally these rules are used by the SLO alerts. +func factorySLIRecordGenerator(slo model.PromSLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { switch { // Event based SLI. case slo.SLI.Events != nil: @@ -78,7 +108,7 @@ func factorySLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBA return nil, fmt.Errorf("invalid SLI type") } -func rawSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { +func rawSLIRecordGenerator(slo model.PromSLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { // Render with our templated data. sliExprTpl := fmt.Sprintf(`(%s)`, slo.SLI.Raw.ErrorRatioQuery) tpl, err := template.New("sliExpr").Option("missingkey=error").Parse(sliExprTpl) @@ -108,7 +138,7 @@ func rawSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlert }, nil } -func eventsSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { +func eventsSLIRecordGenerator(slo model.PromSLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { const sliExprTplFmt = `(%s) / (%s) @@ -150,7 +180,7 @@ func eventsSLIRecordGenerator(slo SLO, window time.Duration, alerts model.MWMBAl // // The way this optimization is made is using one SLI recording rule (the one with the shortest window to // reduce the downsampling, e.g 5m) and make an average over time on that rule for the window time range. -func optimizedSLIRecordGenerator(slo SLO, window, shortWindow time.Duration) (*rulefmt.Rule, error) { +func optimizedSLIRecordGenerator(slo model.PromSLO, window, shortWindow time.Duration) (*rulefmt.Rule, error) { // Averages over ratios (average over average) is statistically incorrect, so we do // aggregate all ratios on the time window and then divide with the aggregation of all the full ratios // that is 1 (thats why we can use `count`), giving use a correct ratio of ratios: @@ -199,120 +229,6 @@ count_over_time({{.metric}}{{.filter}}[{{.window}}]) }, nil } -type metadataRecordingRulesGenerator bool - -// MetadataRecordingRulesGenerator knows how to generate the metadata prometheus recording rules -// from an SLO. -const MetadataRecordingRulesGenerator = metadataRecordingRulesGenerator(false) - -func (m metadataRecordingRulesGenerator) GenerateMetadataRecordingRules(ctx context.Context, info model.Info, slo SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { - labels := utilsdata.MergeLabels(conventions.GetSLOIDPromLabels(slo), slo.Labels) - - // Metatada Recordings. - const ( - metricSLOObjectiveRatio = "slo:objective:ratio" - metricSLOErrorBudgetRatio = "slo:error_budget:ratio" - metricSLOTimePeriodDays = "slo:time_period:days" - metricSLOCurrentBurnRateRatio = "slo:current_burn_rate:ratio" - metricSLOPeriodBurnRateRatio = "slo:period_burn_rate:ratio" - metricSLOPeriodErrorBudgetRemainingRatio = "slo:period_error_budget_remaining:ratio" - metricSLOInfo = "sloth_slo_info" - ) - - sloObjectiveRatio := slo.Objective / 100 - - sloFilter := promutils.LabelsToPromFilter(conventions.GetSLOIDPromLabels(slo)) - - var currentBurnRateExpr bytes.Buffer - err := burnRateRecordingExprTpl.Execute(¤tBurnRateExpr, map[string]string{ - "SLIErrorMetric": conventions.GetSLIErrorMetric(alerts.PageQuick.ShortWindow), - "MetricFilter": sloFilter, - "SLOIDName": conventions.PromSLOIDLabelName, - "SLOLabelName": conventions.PromSLONameLabelName, - "SLOServiceName": conventions.PromSLOServiceLabelName, - "ErrorBudgetRatioMetric": metricSLOErrorBudgetRatio, - }) - if err != nil { - return nil, fmt.Errorf("could not render current burn rate prometheus metadata recording rule expression: %w", err) - } - - var periodBurnRateExpr bytes.Buffer - err = burnRateRecordingExprTpl.Execute(&periodBurnRateExpr, map[string]string{ - "SLIErrorMetric": conventions.GetSLIErrorMetric(slo.TimeWindow), - "MetricFilter": sloFilter, - "SLOIDName": conventions.PromSLOIDLabelName, - "SLOLabelName": conventions.PromSLONameLabelName, - "SLOServiceName": conventions.PromSLOServiceLabelName, - "ErrorBudgetRatioMetric": metricSLOErrorBudgetRatio, - }) - if err != nil { - return nil, fmt.Errorf("could not render period burn rate prometheus metadata recording rule expression: %w", err) - } - - rules := []rulefmt.Rule{ - // SLO Objective. - { - Record: metricSLOObjectiveRatio, - Expr: fmt.Sprintf(`vector(%g)`, sloObjectiveRatio), - Labels: labels, - }, - - // Error budget. - { - Record: metricSLOErrorBudgetRatio, - Expr: fmt.Sprintf(`vector(1-%g)`, sloObjectiveRatio), - Labels: labels, - }, - - // Total period. - { - Record: metricSLOTimePeriodDays, - Expr: fmt.Sprintf(`vector(%g)`, slo.TimeWindow.Hours()/24), - Labels: labels, - }, - - // Current burning speed. - { - Record: metricSLOCurrentBurnRateRatio, - Expr: currentBurnRateExpr.String(), - Labels: labels, - }, - - // Total period burn rate. - { - Record: metricSLOPeriodBurnRateRatio, - Expr: periodBurnRateExpr.String(), - Labels: labels, - }, - - // Total Error budget remaining period. - { - Record: metricSLOPeriodErrorBudgetRemainingRatio, - Expr: fmt.Sprintf(`1 - %s%s`, metricSLOPeriodBurnRateRatio, sloFilter), - Labels: labels, - }, - - // Info. - { - Record: metricSLOInfo, - Expr: `vector(1)`, - Labels: utilsdata.MergeLabels(labels, map[string]string{ - conventions.PromSLOVersionLabelName: info.Version, - conventions.PromSLOModeLabelName: string(info.Mode), - conventions.PromSLOSpecLabelName: info.Spec, - conventions.PromSLOObjectiveLabelName: strconv.FormatFloat(slo.Objective, 'f', -1, 64), - }), - }, - } - - return rules, nil -} - -var burnRateRecordingExprTpl = template.Must(template.New("burnRateExpr").Option("missingkey=error").Parse(`{{ .SLIErrorMetric }}{{ .MetricFilter }} -/ on({{ .SLOIDName }}, {{ .SLOLabelName }}, {{ .SLOServiceName }}) group_left -{{ .ErrorBudgetRatioMetric }}{{ .MetricFilter }} -`)) - // getAlertGroupWindows gets all the time windows from a multiwindow multiburn alert group. func getAlertGroupWindows(alerts model.MWMBAlertGroup) []time.Duration { // Use a map to avoid duplicated windows. diff --git a/internal/prometheus/recording_rules_test.go b/internal/plugin/slo/core/sli_rules_v1/plugin_test.go similarity index 70% rename from internal/prometheus/recording_rules_test.go rename to internal/plugin/slo/core/sli_rules_v1/plugin_test.go index ff1624b6..1ecafd3b 100644 --- a/internal/prometheus/recording_rules_test.go +++ b/internal/plugin/slo/core/sli_rules_v1/plugin_test.go @@ -1,18 +1,21 @@ -package prometheus_test +package plugin_test import ( - "context" + "encoding/json" "testing" "time" "github.com/prometheus/prometheus/model/rulefmt" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - "github.com/slok/sloth/internal/prometheus" + plugin "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" ) -func getAlertGroup() model.MWMBAlertGroup { +func baseAlertGroup() model.MWMBAlertGroup { return model.MWMBAlertGroup{ PageQuick: model.MWMBAlert{ ShortWindow: 5 * time.Minute, @@ -33,27 +36,41 @@ func getAlertGroup() model.MWMBAlertGroup { } } -func TestGenerateSLIRecordingRules(t *testing.T) { - type generator interface { - GenerateSLIRecordingRules(ctx context.Context, slo prometheus.SLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) +func baseSLO() model.PromSLO { + return model.PromSLO{ + ID: "test", + Name: "test-name", + Service: "test-svc", + TimeWindow: 30 * 24 * time.Hour, + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `rate(my_metric[{{.window}}]{error="true"})`, + TotalQuery: `rate(my_metric[{{.window}}])`, + }, + }, + Labels: map[string]string{ + "kind": "test", + }, } +} +func TestGenerateSLIRecordingRules(t *testing.T) { tests := map[string]struct { - generator func() generator - slo prometheus.SLO + optimized bool + slo model.PromSLO alertGroup model.MWMBAlertGroup expRules []rulefmt.Rule expErr bool }{ "Having and SLO with invalid expression should fail.": { - generator: func() generator { return prometheus.OptimizedSLIRecordingRulesGenerator }, - slo: prometheus.SLO{ + optimized: true, + slo: model.PromSLO{ ID: "test", Name: "test-name", Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ ErrorQuery: `rate(my_metric[{{}.window}}]{error="true"})`, TotalQuery: `rate(my_metric[{{.window}}])`, }, @@ -62,19 +79,19 @@ func TestGenerateSLIRecordingRules(t *testing.T) { "kind": "test", }, }, - alertGroup: getAlertGroup(), + alertGroup: baseAlertGroup(), expErr: true, }, "Having and wrong variable in the expression should fail.": { - generator: func() generator { return prometheus.OptimizedSLIRecordingRulesGenerator }, - slo: prometheus.SLO{ + optimized: true, + slo: model.PromSLO{ ID: "test", Name: "test-name", Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ ErrorQuery: `rate(my_metric[{{.Window}}]{error="true"})`, TotalQuery: `rate(my_metric[{{.window}}])`, }, @@ -83,28 +100,14 @@ func TestGenerateSLIRecordingRules(t *testing.T) { "kind": "test", }, }, - alertGroup: getAlertGroup(), + alertGroup: baseAlertGroup(), expErr: true, }, "Having an SLO with SLI(events) and its mwmb alerts should create the recording rules.": { - generator: func() generator { return prometheus.OptimizedSLIRecordingRulesGenerator }, - slo: prometheus.SLO{ - ID: "test", - Name: "test-name", - Service: "test-svc", - TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ - ErrorQuery: `rate(my_metric[{{.window}}]{error="true"})`, - TotalQuery: `rate(my_metric[{{.window}}])`, - }, - }, - Labels: map[string]string{ - "kind": "test", - }, - }, - alertGroup: getAlertGroup(), + optimized: true, + slo: baseSLO(), + alertGroup: baseAlertGroup(), expRules: []rulefmt.Rule{ { Record: "slo:sli_error:ratio_rate5m", @@ -198,14 +201,14 @@ func TestGenerateSLIRecordingRules(t *testing.T) { }, "Having an SLO with SLI(events) and its mwmb alerts should create the recording rules (Non optimized).": { - generator: func() generator { return prometheus.SLIRecordingRulesGenerator }, - slo: prometheus.SLO{ + optimized: false, + slo: model.PromSLO{ ID: "test", Name: "test-name", Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ ErrorQuery: `rate(my_metric[{{.window}}]{error="true"})`, TotalQuery: `rate(my_metric[{{.window}}])`, }, @@ -214,7 +217,7 @@ func TestGenerateSLIRecordingRules(t *testing.T) { "kind": "test", }, }, - alertGroup: getAlertGroup(), + alertGroup: baseAlertGroup(), expRules: []rulefmt.Rule{ { Record: "slo:sli_error:ratio_rate5m", @@ -308,14 +311,14 @@ func TestGenerateSLIRecordingRules(t *testing.T) { }, "Having an SLO with SLI (raw) and its mwmb alerts should create the recording rules.": { - generator: func() generator { return prometheus.OptimizedSLIRecordingRulesGenerator }, - slo: prometheus.SLO{ + optimized: true, + slo: model.PromSLO{ ID: "test", Name: "test-name", Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Raw: &prometheus.SLIRaw{ + SLI: model.PromSLI{ + Raw: &model.PromSLIRaw{ ErrorRatioQuery: `rate(my_metric[{{.window}}])`, }, }, @@ -323,7 +326,7 @@ func TestGenerateSLIRecordingRules(t *testing.T) { "kind": "test", }, }, - alertGroup: getAlertGroup(), + alertGroup: baseAlertGroup(), expRules: []rulefmt.Rule{ { Record: "slo:sli_error:ratio_rate5m", @@ -417,14 +420,14 @@ func TestGenerateSLIRecordingRules(t *testing.T) { }, "An SLO alert with duplicated time windows should appear once and sorted.": { - generator: func() generator { return prometheus.OptimizedSLIRecordingRulesGenerator }, - slo: prometheus.SLO{ + optimized: true, + slo: model.PromSLO{ ID: "test", Name: "test-name", Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ ErrorQuery: `rate(my_metric[{{.window}}]{error="true"})`, TotalQuery: `rate(my_metric[{{.window}}])`, }, @@ -491,139 +494,67 @@ func TestGenerateSLIRecordingRules(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { assert := assert.New(t) + require := require.New(t) + + // Load plugin + config, _ := json.Marshal(map[string]any{"optimized": test.optimized}) + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: config}) + require.NoError(err) - gotRules, err := test.generator().GenerateSLIRecordingRules(context.TODO(), test.slo, test.alertGroup) + // Execute plugin. + req := pluginslov1.Request{ + SLO: test.slo, + MWMBAlertGroup: test.alertGroup, + } + res := pluginslov1.Result{} + err = plugin.ProcessSLO(t.Context(), &req, &res) + // Check result. if test.expErr { assert.Error(err) } else if assert.NoError(err) { - assert.Equal(test.expRules, gotRules) + assert.Equal(test.expRules, res.SLORules.SLIErrorRecRules.Rules) } }) } } -func TestGenerateMetaRecordingRules(t *testing.T) { - tests := map[string]struct { - info model.Info - slo prometheus.SLO - alertGroup model.MWMBAlertGroup - expRules []rulefmt.Rule - expErr bool - }{ - "Having and SLO an its mwmb alerts should create the metadata recording rules.": { - info: model.Info{ - Version: "test-ver", - Mode: model.ModeTest, - Spec: "test/v1", - }, - slo: prometheus.SLO{ - ID: "test", - Name: "test-name", - Service: "test-svc", - Objective: 99.9, - TimeWindow: 30 * 24 * time.Hour, - Labels: map[string]string{ - "kind": "test", - }, - }, - alertGroup: getAlertGroup(), - expRules: []rulefmt.Rule{ - { - Record: "slo:objective:ratio", - Expr: "vector(0.9990000000000001)", - Labels: map[string]string{ - "kind": "test", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test", - }, - }, - { - Record: "slo:error_budget:ratio", - Expr: "vector(1-0.9990000000000001)", - Labels: map[string]string{ - "kind": "test", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test", - }, - }, - { - Record: "slo:time_period:days", - Expr: "vector(30)", - Labels: map[string]string{ - "kind": "test", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test", - }, - }, - { - Record: "slo:current_burn_rate:ratio", - Expr: `slo:sli_error:ratio_rate5m{sloth_id="test", sloth_service="test-svc", sloth_slo="test-name"} -/ on(sloth_id, sloth_slo, sloth_service) group_left -slo:error_budget:ratio{sloth_id="test", sloth_service="test-svc", sloth_slo="test-name"} -`, - Labels: map[string]string{ - "kind": "test", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test", - }, - }, - { - Record: "slo:period_burn_rate:ratio", - Expr: `slo:sli_error:ratio_rate30d{sloth_id="test", sloth_service="test-svc", sloth_slo="test-name"} -/ on(sloth_id, sloth_slo, sloth_service) group_left -slo:error_budget:ratio{sloth_id="test", sloth_service="test-svc", sloth_slo="test-name"} -`, - Labels: map[string]string{ - "kind": "test", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test", - }, - }, - { - Record: "slo:period_error_budget_remaining:ratio", - Expr: `1 - slo:period_burn_rate:ratio{sloth_id="test", sloth_service="test-svc", sloth_slo="test-name"}`, - Labels: map[string]string{ - "kind": "test", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test", - }, - }, - { - Record: "sloth_slo_info", - Expr: `vector(1)`, - Labels: map[string]string{ - "kind": "test", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test", - "sloth_version": "test-ver", - "sloth_mode": "test", - "sloth_spec": "test/v1", - "sloth_objective": "99.9", - }, - }, - }, - }, +func BenchmarkPluginYaegi(b *testing.B) { + config, _ := json.Marshal(map[string]any{"optimized": true}) + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{ + PluginConfiguration: config, + }) + if err != nil { + b.Fatal(err) } - for name, test := range tests { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) + req := &pluginslov1.Request{ + SLO: baseSLO(), + MWMBAlertGroup: baseAlertGroup(), + } + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), req, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} - gotRules, err := prometheus.MetadataRecordingRulesGenerator.GenerateMetadataRecordingRules(context.TODO(), test.info, test.slo, test.alertGroup) +func BenchmarkPluginGo(b *testing.B) { + config, _ := json.Marshal(map[string]any{"optimized": true}) + plugin, err := plugin.NewPlugin(config, pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } - if test.expErr { - assert.Error(err) - } else if assert.NoError(err) { - assert.Equal(test.expRules, gotRules) - } - }) + req := &pluginslov1.Request{ + SLO: baseSLO(), + MWMBAlertGroup: baseAlertGroup(), + } + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), req, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } } } diff --git a/internal/plugin/slo/custom/custom.go b/internal/plugin/slo/custom/custom.go new file mode 100644 index 00000000..f4728fa7 --- /dev/null +++ b/internal/plugin/slo/custom/custom.go @@ -0,0 +1,11 @@ +package custom + +import ( + "reflect" +) + +//go:generate yaegi extract --name custom github.com/prometheus/common/model github.com/prometheus/prometheus/model/rulefmt github.com/prometheus/prometheus/promql/parser +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/prometheus/plugin/slo/v1 github.com/slok/sloth/pkg/common/conventions github.com/slok/sloth/pkg/common/model github.com/slok/sloth/pkg/common/utils/data github.com/slok/sloth/pkg/common/utils/prometheus + +// Symbols variable stores the map of custom Yaegi symbols per package. +var Symbols = map[string]map[string]reflect.Value{} diff --git a/internal/plugin/slo/custom/github_com-prometheus-common-model.go b/internal/plugin/slo/custom/github_com-prometheus-common-model.go new file mode 100644 index 00000000..25cd0908 --- /dev/null +++ b/internal/plugin/slo/custom/github_com-prometheus-common-model.go @@ -0,0 +1,142 @@ +// Code generated by 'yaegi extract github.com/prometheus/common/model'. DO NOT EDIT. + +package custom + +import ( + "github.com/prometheus/common/model" + "go/constant" + "go/token" + "reflect" +) + +func init() { + Symbols["github.com/prometheus/common/model/model"] = map[string]reflect.Value{ + // function, constant and variable definitions + "AddressLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__address__\"", token.STRING, 0)), + "AlertFiring": reflect.ValueOf(model.AlertFiring), + "AlertNameLabel": reflect.ValueOf(constant.MakeFromLiteral("\"alertname\"", token.STRING, 0)), + "AlertResolved": reflect.ValueOf(model.AlertResolved), + "AllowUTF8": reflect.ValueOf(constant.MakeFromLiteral("\"allow-utf-8\"", token.STRING, 0)), + "BucketLabel": reflect.ValueOf(constant.MakeFromLiteral("\"le\"", token.STRING, 0)), + "DotsEscaping": reflect.ValueOf(model.DotsEscaping), + "Earliest": reflect.ValueOf(model.Earliest), + "EscapeDots": reflect.ValueOf(constant.MakeFromLiteral("\"dots\"", token.STRING, 0)), + "EscapeMetricFamily": reflect.ValueOf(model.EscapeMetricFamily), + "EscapeName": reflect.ValueOf(model.EscapeName), + "EscapeUnderscores": reflect.ValueOf(constant.MakeFromLiteral("\"underscores\"", token.STRING, 0)), + "EscapeValues": reflect.ValueOf(constant.MakeFromLiteral("\"values\"", token.STRING, 0)), + "EscapingKey": reflect.ValueOf(constant.MakeFromLiteral("\"escaping\"", token.STRING, 0)), + "ExportedLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"exported_\"", token.STRING, 0)), + "FingerprintFromString": reflect.ValueOf(model.FingerprintFromString), + "InstanceLabel": reflect.ValueOf(constant.MakeFromLiteral("\"instance\"", token.STRING, 0)), + "IsValidLegacyMetricName": reflect.ValueOf(model.IsValidLegacyMetricName), + "IsValidMetricName": reflect.ValueOf(model.IsValidMetricName), + "JobLabel": reflect.ValueOf(constant.MakeFromLiteral("\"job\"", token.STRING, 0)), + "LabelNameRE": reflect.ValueOf(&model.LabelNameRE).Elem(), + "LabelsToSignature": reflect.ValueOf(model.LabelsToSignature), + "Latest": reflect.ValueOf(model.Latest), + "LegacyValidation": reflect.ValueOf(model.LegacyValidation), + "MetaLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__meta_\"", token.STRING, 0)), + "MetricNameLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__name__\"", token.STRING, 0)), + "MetricNameRE": reflect.ValueOf(&model.MetricNameRE).Elem(), + "MetricTypeCounter": reflect.ValueOf(model.MetricTypeCounter), + "MetricTypeGauge": reflect.ValueOf(model.MetricTypeGauge), + "MetricTypeGaugeHistogram": reflect.ValueOf(model.MetricTypeGaugeHistogram), + "MetricTypeHistogram": reflect.ValueOf(model.MetricTypeHistogram), + "MetricTypeInfo": reflect.ValueOf(model.MetricTypeInfo), + "MetricTypeStateset": reflect.ValueOf(model.MetricTypeStateset), + "MetricTypeSummary": reflect.ValueOf(model.MetricTypeSummary), + "MetricTypeUnknown": reflect.ValueOf(model.MetricTypeUnknown), + "MetricsPathLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__metrics_path__\"", token.STRING, 0)), + "NameEscapingScheme": reflect.ValueOf(&model.NameEscapingScheme).Elem(), + "NameValidationScheme": reflect.ValueOf(&model.NameValidationScheme).Elem(), + "NoEscaping": reflect.ValueOf(model.NoEscaping), + "Now": reflect.ValueOf(model.Now), + "ParamLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__param_\"", token.STRING, 0)), + "ParseDuration": reflect.ValueOf(model.ParseDuration), + "ParseFingerprint": reflect.ValueOf(model.ParseFingerprint), + "QuantileLabel": reflect.ValueOf(constant.MakeFromLiteral("\"quantile\"", token.STRING, 0)), + "ReservedLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__\"", token.STRING, 0)), + "SchemeLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__scheme__\"", token.STRING, 0)), + "ScrapeIntervalLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__scrape_interval__\"", token.STRING, 0)), + "ScrapeTimeoutLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__scrape_timeout__\"", token.STRING, 0)), + "SeparatorByte": reflect.ValueOf(model.SeparatorByte), + "SignatureForLabels": reflect.ValueOf(model.SignatureForLabels), + "SignatureWithoutLabels": reflect.ValueOf(model.SignatureWithoutLabels), + "TimeFromUnix": reflect.ValueOf(model.TimeFromUnix), + "TimeFromUnixNano": reflect.ValueOf(model.TimeFromUnixNano), + "TmpLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__tmp_\"", token.STRING, 0)), + "ToEscapingScheme": reflect.ValueOf(model.ToEscapingScheme), + "UTF8Validation": reflect.ValueOf(model.UTF8Validation), + "UnderscoreEscaping": reflect.ValueOf(model.UnderscoreEscaping), + "UnescapeName": reflect.ValueOf(model.UnescapeName), + "ValMatrix": reflect.ValueOf(model.ValMatrix), + "ValNone": reflect.ValueOf(model.ValNone), + "ValScalar": reflect.ValueOf(model.ValScalar), + "ValString": reflect.ValueOf(model.ValString), + "ValVector": reflect.ValueOf(model.ValVector), + "ValueEncodingEscaping": reflect.ValueOf(model.ValueEncodingEscaping), + "ZeroSample": reflect.ValueOf(&model.ZeroSample).Elem(), + "ZeroSamplePair": reflect.ValueOf(&model.ZeroSamplePair).Elem(), + + // type definitions + "Alert": reflect.ValueOf((*model.Alert)(nil)), + "AlertStatus": reflect.ValueOf((*model.AlertStatus)(nil)), + "Alerts": reflect.ValueOf((*model.Alerts)(nil)), + "Duration": reflect.ValueOf((*model.Duration)(nil)), + "EscapingScheme": reflect.ValueOf((*model.EscapingScheme)(nil)), + "Fingerprint": reflect.ValueOf((*model.Fingerprint)(nil)), + "FingerprintSet": reflect.ValueOf((*model.FingerprintSet)(nil)), + "Fingerprints": reflect.ValueOf((*model.Fingerprints)(nil)), + "FloatString": reflect.ValueOf((*model.FloatString)(nil)), + "HistogramBucket": reflect.ValueOf((*model.HistogramBucket)(nil)), + "HistogramBuckets": reflect.ValueOf((*model.HistogramBuckets)(nil)), + "Interval": reflect.ValueOf((*model.Interval)(nil)), + "LabelName": reflect.ValueOf((*model.LabelName)(nil)), + "LabelNames": reflect.ValueOf((*model.LabelNames)(nil)), + "LabelPair": reflect.ValueOf((*model.LabelPair)(nil)), + "LabelPairs": reflect.ValueOf((*model.LabelPairs)(nil)), + "LabelSet": reflect.ValueOf((*model.LabelSet)(nil)), + "LabelValue": reflect.ValueOf((*model.LabelValue)(nil)), + "LabelValues": reflect.ValueOf((*model.LabelValues)(nil)), + "Matcher": reflect.ValueOf((*model.Matcher)(nil)), + "Matrix": reflect.ValueOf((*model.Matrix)(nil)), + "Metric": reflect.ValueOf((*model.Metric)(nil)), + "MetricType": reflect.ValueOf((*model.MetricType)(nil)), + "Sample": reflect.ValueOf((*model.Sample)(nil)), + "SampleHistogram": reflect.ValueOf((*model.SampleHistogram)(nil)), + "SampleHistogramPair": reflect.ValueOf((*model.SampleHistogramPair)(nil)), + "SamplePair": reflect.ValueOf((*model.SamplePair)(nil)), + "SampleStream": reflect.ValueOf((*model.SampleStream)(nil)), + "SampleValue": reflect.ValueOf((*model.SampleValue)(nil)), + "Samples": reflect.ValueOf((*model.Samples)(nil)), + "Scalar": reflect.ValueOf((*model.Scalar)(nil)), + "Silence": reflect.ValueOf((*model.Silence)(nil)), + "String": reflect.ValueOf((*model.String)(nil)), + "Time": reflect.ValueOf((*model.Time)(nil)), + "ValidationScheme": reflect.ValueOf((*model.ValidationScheme)(nil)), + "Value": reflect.ValueOf((*model.Value)(nil)), + "ValueType": reflect.ValueOf((*model.ValueType)(nil)), + "Vector": reflect.ValueOf((*model.Vector)(nil)), + + // interface wrapper definitions + "_Value": reflect.ValueOf((*_github_com_prometheus_common_model_Value)(nil)), + } +} + +// _github_com_prometheus_common_model_Value is an interface wrapper for Value type +type _github_com_prometheus_common_model_Value struct { + IValue interface{} + WString func() string + WType func() model.ValueType +} + +func (W _github_com_prometheus_common_model_Value) String() string { + if W.WString == nil { + return "" + } + return W.WString() +} +func (W _github_com_prometheus_common_model_Value) Type() model.ValueType { + return W.WType() +} diff --git a/internal/plugin/slo/custom/github_com-prometheus-prometheus-model-rulefmt.go b/internal/plugin/slo/custom/github_com-prometheus-prometheus-model-rulefmt.go new file mode 100644 index 00000000..01b9ac4b --- /dev/null +++ b/internal/plugin/slo/custom/github_com-prometheus-prometheus-model-rulefmt.go @@ -0,0 +1,25 @@ +// Code generated by 'yaegi extract github.com/prometheus/prometheus/model/rulefmt'. DO NOT EDIT. + +package custom + +import ( + "github.com/prometheus/prometheus/model/rulefmt" + "reflect" +) + +func init() { + Symbols["github.com/prometheus/prometheus/model/rulefmt/rulefmt"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Parse": reflect.ValueOf(rulefmt.Parse), + "ParseFile": reflect.ValueOf(rulefmt.ParseFile), + + // type definitions + "Error": reflect.ValueOf((*rulefmt.Error)(nil)), + "Rule": reflect.ValueOf((*rulefmt.Rule)(nil)), + "RuleGroup": reflect.ValueOf((*rulefmt.RuleGroup)(nil)), + "RuleGroupNode": reflect.ValueOf((*rulefmt.RuleGroupNode)(nil)), + "RuleGroups": reflect.ValueOf((*rulefmt.RuleGroups)(nil)), + "RuleNode": reflect.ValueOf((*rulefmt.RuleNode)(nil)), + "WrappedError": reflect.ValueOf((*rulefmt.WrappedError)(nil)), + } +} diff --git a/internal/plugin/slo/custom/github_com-prometheus-prometheus-promql-parser.go b/internal/plugin/slo/custom/github_com-prometheus-prometheus-promql-parser.go new file mode 100644 index 00000000..be0f437c --- /dev/null +++ b/internal/plugin/slo/custom/github_com-prometheus-prometheus-promql-parser.go @@ -0,0 +1,289 @@ +// Code generated by 'yaegi extract github.com/prometheus/prometheus/promql/parser'. DO NOT EDIT. + +package custom + +import ( + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/promql/parser/posrange" + "go/constant" + "go/token" + "reflect" +) + +func init() { + Symbols["github.com/prometheus/prometheus/promql/parser/parser"] = map[string]reflect.Value{ + // function, constant and variable definitions + "ADD": reflect.ValueOf(constant.MakeFromLiteral("57383", token.INT, 0)), + "AT": reflect.ValueOf(constant.MakeFromLiteral("57400", token.INT, 0)), + "ATAN2": reflect.ValueOf(constant.MakeFromLiteral("57401", token.INT, 0)), + "AVG": reflect.ValueOf(constant.MakeFromLiteral("57404", token.INT, 0)), + "BLANK": reflect.ValueOf(constant.MakeFromLiteral("57347", token.INT, 0)), + "BOOL": reflect.ValueOf(constant.MakeFromLiteral("57420", token.INT, 0)), + "BOTTOMK": reflect.ValueOf(constant.MakeFromLiteral("57405", token.INT, 0)), + "BUCKETS_DESC": reflect.ValueOf(constant.MakeFromLiteral("57375", token.INT, 0)), + "BY": reflect.ValueOf(constant.MakeFromLiteral("57421", token.INT, 0)), + "CLOSE_HIST": reflect.ValueOf(constant.MakeFromLiteral("57359", token.INT, 0)), + "COLON": reflect.ValueOf(constant.MakeFromLiteral("57348", token.INT, 0)), + "COMMA": reflect.ValueOf(constant.MakeFromLiteral("57349", token.INT, 0)), + "COMMENT": reflect.ValueOf(constant.MakeFromLiteral("57350", token.INT, 0)), + "COUNT": reflect.ValueOf(constant.MakeFromLiteral("57406", token.INT, 0)), + "COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57435", token.INT, 0)), + "COUNTER_RESET_HINT_DESC": reflect.ValueOf(constant.MakeFromLiteral("57380", token.INT, 0)), + "COUNT_DESC": reflect.ValueOf(constant.MakeFromLiteral("57371", token.INT, 0)), + "COUNT_VALUES": reflect.ValueOf(constant.MakeFromLiteral("57407", token.INT, 0)), + "CUSTOM_VALUES_DESC": reflect.ValueOf(constant.MakeFromLiteral("57379", token.INT, 0)), + "CardManyToMany": reflect.ValueOf(parser.CardManyToMany), + "CardManyToOne": reflect.ValueOf(parser.CardManyToOne), + "CardOneToMany": reflect.ValueOf(parser.CardOneToMany), + "CardOneToOne": reflect.ValueOf(parser.CardOneToOne), + "Children": reflect.ValueOf(parser.Children), + "DIV": reflect.ValueOf(constant.MakeFromLiteral("57384", token.INT, 0)), + "DURATION": reflect.ValueOf(constant.MakeFromLiteral("57351", token.INT, 0)), + "DocumentedType": reflect.ValueOf(parser.DocumentedType), + "END": reflect.ValueOf(constant.MakeFromLiteral("57431", token.INT, 0)), + "EOF": reflect.ValueOf(constant.MakeFromLiteral("57352", token.INT, 0)), + "EQL": reflect.ValueOf(constant.MakeFromLiteral("57346", token.INT, 0)), + "EQLC": reflect.ValueOf(constant.MakeFromLiteral("57385", token.INT, 0)), + "EQL_REGEX": reflect.ValueOf(constant.MakeFromLiteral("57386", token.INT, 0)), + "ERROR": reflect.ValueOf(constant.MakeFromLiteral("57353", token.INT, 0)), + "EnableExperimentalFunctions": reflect.ValueOf(&parser.EnableExperimentalFunctions).Elem(), + "EnrichParseError": reflect.ValueOf(parser.EnrichParseError), + "ExtractSelectors": reflect.ValueOf(parser.ExtractSelectors), + "Functions": reflect.ValueOf(&parser.Functions).Elem(), + "GAUGE_TYPE": reflect.ValueOf(constant.MakeFromLiteral("57437", token.INT, 0)), + "GROUP": reflect.ValueOf(constant.MakeFromLiteral("57408", token.INT, 0)), + "GROUP_LEFT": reflect.ValueOf(constant.MakeFromLiteral("57422", token.INT, 0)), + "GROUP_RIGHT": reflect.ValueOf(constant.MakeFromLiteral("57423", token.INT, 0)), + "GTE": reflect.ValueOf(constant.MakeFromLiteral("57387", token.INT, 0)), + "GTR": reflect.ValueOf(constant.MakeFromLiteral("57388", token.INT, 0)), + "IDENTIFIER": reflect.ValueOf(constant.MakeFromLiteral("57354", token.INT, 0)), + "IGNORING": reflect.ValueOf(constant.MakeFromLiteral("57424", token.INT, 0)), + "Inspect": reflect.ValueOf(parser.Inspect), + "ItemTypeStr": reflect.ValueOf(&parser.ItemTypeStr).Elem(), + "LAND": reflect.ValueOf(constant.MakeFromLiteral("57389", token.INT, 0)), + "LEFT_BRACE": reflect.ValueOf(constant.MakeFromLiteral("57355", token.INT, 0)), + "LEFT_BRACKET": reflect.ValueOf(constant.MakeFromLiteral("57356", token.INT, 0)), + "LEFT_PAREN": reflect.ValueOf(constant.MakeFromLiteral("57357", token.INT, 0)), + "LIMITK": reflect.ValueOf(constant.MakeFromLiteral("57416", token.INT, 0)), + "LIMIT_RATIO": reflect.ValueOf(constant.MakeFromLiteral("57417", token.INT, 0)), + "LOR": reflect.ValueOf(constant.MakeFromLiteral("57390", token.INT, 0)), + "LSS": reflect.ValueOf(constant.MakeFromLiteral("57391", token.INT, 0)), + "LTE": reflect.ValueOf(constant.MakeFromLiteral("57392", token.INT, 0)), + "LUNLESS": reflect.ValueOf(constant.MakeFromLiteral("57393", token.INT, 0)), + "Lex": reflect.ValueOf(parser.Lex), + "MAX": reflect.ValueOf(constant.MakeFromLiteral("57409", token.INT, 0)), + "METRIC_IDENTIFIER": reflect.ValueOf(constant.MakeFromLiteral("57360", token.INT, 0)), + "MIN": reflect.ValueOf(constant.MakeFromLiteral("57410", token.INT, 0)), + "MOD": reflect.ValueOf(constant.MakeFromLiteral("57394", token.INT, 0)), + "MUL": reflect.ValueOf(constant.MakeFromLiteral("57395", token.INT, 0)), + "MustGetFunction": reflect.ValueOf(parser.MustGetFunction), + "MustLabelMatcher": reflect.ValueOf(parser.MustLabelMatcher), + "NEGATIVE_BUCKETS_DESC": reflect.ValueOf(constant.MakeFromLiteral("57376", token.INT, 0)), + "NEGATIVE_OFFSET_DESC": reflect.ValueOf(constant.MakeFromLiteral("57374", token.INT, 0)), + "NEQ": reflect.ValueOf(constant.MakeFromLiteral("57396", token.INT, 0)), + "NEQ_REGEX": reflect.ValueOf(constant.MakeFromLiteral("57397", token.INT, 0)), + "NOT_COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57436", token.INT, 0)), + "NUMBER": reflect.ValueOf(constant.MakeFromLiteral("57361", token.INT, 0)), + "NewParser": reflect.ValueOf(parser.NewParser), + "OFFSET": reflect.ValueOf(constant.MakeFromLiteral("57425", token.INT, 0)), + "OFFSET_DESC": reflect.ValueOf(constant.MakeFromLiteral("57373", token.INT, 0)), + "ON": reflect.ValueOf(constant.MakeFromLiteral("57426", token.INT, 0)), + "OPEN_HIST": reflect.ValueOf(constant.MakeFromLiteral("57358", token.INT, 0)), + "POW": reflect.ValueOf(constant.MakeFromLiteral("57398", token.INT, 0)), + "ParseExpr": reflect.ValueOf(parser.ParseExpr), + "ParseMetric": reflect.ValueOf(parser.ParseMetric), + "ParseMetricSelector": reflect.ValueOf(parser.ParseMetricSelector), + "ParseMetricSelectors": reflect.ValueOf(parser.ParseMetricSelectors), + "ParseSeriesDesc": reflect.ValueOf(parser.ParseSeriesDesc), + "Prettify": reflect.ValueOf(parser.Prettify), + "QUANTILE": reflect.ValueOf(constant.MakeFromLiteral("57411", token.INT, 0)), + "RIGHT_BRACE": reflect.ValueOf(constant.MakeFromLiteral("57362", token.INT, 0)), + "RIGHT_BRACKET": reflect.ValueOf(constant.MakeFromLiteral("57363", token.INT, 0)), + "RIGHT_PAREN": reflect.ValueOf(constant.MakeFromLiteral("57364", token.INT, 0)), + "SCHEMA_DESC": reflect.ValueOf(constant.MakeFromLiteral("57372", token.INT, 0)), + "SEMICOLON": reflect.ValueOf(constant.MakeFromLiteral("57365", token.INT, 0)), + "SPACE": reflect.ValueOf(constant.MakeFromLiteral("57366", token.INT, 0)), + "START": reflect.ValueOf(constant.MakeFromLiteral("57430", token.INT, 0)), + "START_EXPRESSION": reflect.ValueOf(constant.MakeFromLiteral("57442", token.INT, 0)), + "START_METRIC": reflect.ValueOf(constant.MakeFromLiteral("57440", token.INT, 0)), + "START_METRIC_SELECTOR": reflect.ValueOf(constant.MakeFromLiteral("57443", token.INT, 0)), + "START_SERIES_DESCRIPTION": reflect.ValueOf(constant.MakeFromLiteral("57441", token.INT, 0)), + "STDDEV": reflect.ValueOf(constant.MakeFromLiteral("57412", token.INT, 0)), + "STDVAR": reflect.ValueOf(constant.MakeFromLiteral("57413", token.INT, 0)), + "STRING": reflect.ValueOf(constant.MakeFromLiteral("57367", token.INT, 0)), + "SUB": reflect.ValueOf(constant.MakeFromLiteral("57399", token.INT, 0)), + "SUM": reflect.ValueOf(constant.MakeFromLiteral("57414", token.INT, 0)), + "SUM_DESC": reflect.ValueOf(constant.MakeFromLiteral("57370", token.INT, 0)), + "TIMES": reflect.ValueOf(constant.MakeFromLiteral("57368", token.INT, 0)), + "TOPK": reflect.ValueOf(constant.MakeFromLiteral("57415", token.INT, 0)), + "Tree": reflect.ValueOf(parser.Tree), + "UNKNOWN_COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57434", token.INT, 0)), + "ValueTypeMatrix": reflect.ValueOf(parser.ValueTypeMatrix), + "ValueTypeNone": reflect.ValueOf(parser.ValueTypeNone), + "ValueTypeScalar": reflect.ValueOf(parser.ValueTypeScalar), + "ValueTypeString": reflect.ValueOf(parser.ValueTypeString), + "ValueTypeVector": reflect.ValueOf(parser.ValueTypeVector), + "WITHOUT": reflect.ValueOf(constant.MakeFromLiteral("57427", token.INT, 0)), + "Walk": reflect.ValueOf(parser.Walk), + "WithFunctions": reflect.ValueOf(parser.WithFunctions), + "ZERO_BUCKET_DESC": reflect.ValueOf(constant.MakeFromLiteral("57377", token.INT, 0)), + "ZERO_BUCKET_WIDTH_DESC": reflect.ValueOf(constant.MakeFromLiteral("57378", token.INT, 0)), + + // type definitions + "AggregateExpr": reflect.ValueOf((*parser.AggregateExpr)(nil)), + "BinaryExpr": reflect.ValueOf((*parser.BinaryExpr)(nil)), + "Call": reflect.ValueOf((*parser.Call)(nil)), + "EvalStmt": reflect.ValueOf((*parser.EvalStmt)(nil)), + "Expr": reflect.ValueOf((*parser.Expr)(nil)), + "Expressions": reflect.ValueOf((*parser.Expressions)(nil)), + "Function": reflect.ValueOf((*parser.Function)(nil)), + "Item": reflect.ValueOf((*parser.Item)(nil)), + "ItemType": reflect.ValueOf((*parser.ItemType)(nil)), + "Lexer": reflect.ValueOf((*parser.Lexer)(nil)), + "MatrixSelector": reflect.ValueOf((*parser.MatrixSelector)(nil)), + "Node": reflect.ValueOf((*parser.Node)(nil)), + "NumberLiteral": reflect.ValueOf((*parser.NumberLiteral)(nil)), + "Opt": reflect.ValueOf((*parser.Opt)(nil)), + "ParenExpr": reflect.ValueOf((*parser.ParenExpr)(nil)), + "ParseErr": reflect.ValueOf((*parser.ParseErr)(nil)), + "ParseErrors": reflect.ValueOf((*parser.ParseErrors)(nil)), + "Parser": reflect.ValueOf((*parser.Parser)(nil)), + "SequenceValue": reflect.ValueOf((*parser.SequenceValue)(nil)), + "Statement": reflect.ValueOf((*parser.Statement)(nil)), + "StepInvariantExpr": reflect.ValueOf((*parser.StepInvariantExpr)(nil)), + "StringLiteral": reflect.ValueOf((*parser.StringLiteral)(nil)), + "SubqueryExpr": reflect.ValueOf((*parser.SubqueryExpr)(nil)), + "TestStmt": reflect.ValueOf((*parser.TestStmt)(nil)), + "UnaryExpr": reflect.ValueOf((*parser.UnaryExpr)(nil)), + "Value": reflect.ValueOf((*parser.Value)(nil)), + "ValueType": reflect.ValueOf((*parser.ValueType)(nil)), + "VectorMatchCardinality": reflect.ValueOf((*parser.VectorMatchCardinality)(nil)), + "VectorMatching": reflect.ValueOf((*parser.VectorMatching)(nil)), + "VectorSelector": reflect.ValueOf((*parser.VectorSelector)(nil)), + "Visitor": reflect.ValueOf((*parser.Visitor)(nil)), + + // interface wrapper definitions + "_Expr": reflect.ValueOf((*_github_com_prometheus_prometheus_promql_parser_Expr)(nil)), + "_Node": reflect.ValueOf((*_github_com_prometheus_prometheus_promql_parser_Node)(nil)), + "_Parser": reflect.ValueOf((*_github_com_prometheus_prometheus_promql_parser_Parser)(nil)), + "_Statement": reflect.ValueOf((*_github_com_prometheus_prometheus_promql_parser_Statement)(nil)), + "_Value": reflect.ValueOf((*_github_com_prometheus_prometheus_promql_parser_Value)(nil)), + "_Visitor": reflect.ValueOf((*_github_com_prometheus_prometheus_promql_parser_Visitor)(nil)), + } +} + +// _github_com_prometheus_prometheus_promql_parser_Expr is an interface wrapper for Expr type +type _github_com_prometheus_prometheus_promql_parser_Expr struct { + IValue interface{} + WPositionRange func() posrange.PositionRange + WPretty func(level int) string + WPromQLExpr func() + WString func() string + WType func() parser.ValueType +} + +func (W _github_com_prometheus_prometheus_promql_parser_Expr) PositionRange() posrange.PositionRange { + return W.WPositionRange() +} +func (W _github_com_prometheus_prometheus_promql_parser_Expr) Pretty(level int) string { + return W.WPretty(level) +} +func (W _github_com_prometheus_prometheus_promql_parser_Expr) PromQLExpr() { + W.WPromQLExpr() +} +func (W _github_com_prometheus_prometheus_promql_parser_Expr) String() string { + if W.WString == nil { + return "" + } + return W.WString() +} +func (W _github_com_prometheus_prometheus_promql_parser_Expr) Type() parser.ValueType { + return W.WType() +} + +// _github_com_prometheus_prometheus_promql_parser_Node is an interface wrapper for Node type +type _github_com_prometheus_prometheus_promql_parser_Node struct { + IValue interface{} + WPositionRange func() posrange.PositionRange + WPretty func(level int) string + WString func() string +} + +func (W _github_com_prometheus_prometheus_promql_parser_Node) PositionRange() posrange.PositionRange { + return W.WPositionRange() +} +func (W _github_com_prometheus_prometheus_promql_parser_Node) Pretty(level int) string { + return W.WPretty(level) +} +func (W _github_com_prometheus_prometheus_promql_parser_Node) String() string { + if W.WString == nil { + return "" + } + return W.WString() +} + +// _github_com_prometheus_prometheus_promql_parser_Parser is an interface wrapper for Parser type +type _github_com_prometheus_prometheus_promql_parser_Parser struct { + IValue interface{} + WClose func() + WParseExpr func() (parser.Expr, error) +} + +func (W _github_com_prometheus_prometheus_promql_parser_Parser) Close() { + W.WClose() +} +func (W _github_com_prometheus_prometheus_promql_parser_Parser) ParseExpr() (parser.Expr, error) { + return W.WParseExpr() +} + +// _github_com_prometheus_prometheus_promql_parser_Statement is an interface wrapper for Statement type +type _github_com_prometheus_prometheus_promql_parser_Statement struct { + IValue interface{} + WPositionRange func() posrange.PositionRange + WPretty func(level int) string + WPromQLStmt func() + WString func() string +} + +func (W _github_com_prometheus_prometheus_promql_parser_Statement) PositionRange() posrange.PositionRange { + return W.WPositionRange() +} +func (W _github_com_prometheus_prometheus_promql_parser_Statement) Pretty(level int) string { + return W.WPretty(level) +} +func (W _github_com_prometheus_prometheus_promql_parser_Statement) PromQLStmt() { + W.WPromQLStmt() +} +func (W _github_com_prometheus_prometheus_promql_parser_Statement) String() string { + if W.WString == nil { + return "" + } + return W.WString() +} + +// _github_com_prometheus_prometheus_promql_parser_Value is an interface wrapper for Value type +type _github_com_prometheus_prometheus_promql_parser_Value struct { + IValue interface{} + WString func() string + WType func() parser.ValueType +} + +func (W _github_com_prometheus_prometheus_promql_parser_Value) String() string { + if W.WString == nil { + return "" + } + return W.WString() +} +func (W _github_com_prometheus_prometheus_promql_parser_Value) Type() parser.ValueType { + return W.WType() +} + +// _github_com_prometheus_prometheus_promql_parser_Visitor is an interface wrapper for Visitor type +type _github_com_prometheus_prometheus_promql_parser_Visitor struct { + IValue interface{} + WVisit func(node parser.Node, path []parser.Node) (w parser.Visitor, err error) +} + +func (W _github_com_prometheus_prometheus_promql_parser_Visitor) Visit(node parser.Node, path []parser.Node) (w parser.Visitor, err error) { + return W.WVisit(node, path) +} diff --git a/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-conventions.go b/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-conventions.go new file mode 100644 index 00000000..f31d3f03 --- /dev/null +++ b/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-conventions.go @@ -0,0 +1,28 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/conventions'. DO NOT EDIT. + +package custom + +import ( + "github.com/slok/sloth/pkg/common/conventions" + "go/constant" + "go/token" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/common/conventions/conventions"] = map[string]reflect.Value{ + // function, constant and variable definitions + "GetSLIErrorMetric": reflect.ValueOf(conventions.GetSLIErrorMetric), + "GetSLOIDPromLabels": reflect.ValueOf(conventions.GetSLOIDPromLabels), + "PromSLIErrorMetricFmt": reflect.ValueOf(constant.MakeFromLiteral("\"slo:sli_error:ratio_rate%s\"", token.STRING, 0)), + "PromSLOIDLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_id\"", token.STRING, 0)), + "PromSLOModeLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_mode\"", token.STRING, 0)), + "PromSLONameLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_slo\"", token.STRING, 0)), + "PromSLOObjectiveLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_objective\"", token.STRING, 0)), + "PromSLOServiceLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_service\"", token.STRING, 0)), + "PromSLOSeverityLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_severity\"", token.STRING, 0)), + "PromSLOSpecLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_spec\"", token.STRING, 0)), + "PromSLOVersionLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_version\"", token.STRING, 0)), + "PromSLOWindowLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_window\"", token.STRING, 0)), + } +} diff --git a/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-model.go b/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-model.go new file mode 100644 index 00000000..cf4fc0f8 --- /dev/null +++ b/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-model.go @@ -0,0 +1,41 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/model'. DO NOT EDIT. + +package custom + +import ( + "github.com/slok/sloth/pkg/common/model" + "go/constant" + "go/token" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/common/model/model"] = map[string]reflect.Value{ + // function, constant and variable definitions + "ModeCLIGenKubernetes": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-k8s\"", token.STRING, 0)), + "ModeCLIGenOpenSLO": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-openslo\"", token.STRING, 0)), + "ModeCLIGenPrometheus": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-prom\"", token.STRING, 0)), + "ModeControllerGenKubernetes": reflect.ValueOf(constant.MakeFromLiteral("\"ctrl-gen-k8s\"", token.STRING, 0)), + "ModeTest": reflect.ValueOf(constant.MakeFromLiteral("\"test\"", token.STRING, 0)), + "PageAlertSeverity": reflect.ValueOf(model.PageAlertSeverity), + "PromQueryTPLKeyWindow": reflect.ValueOf(constant.MakeFromLiteral("\"window\"", token.STRING, 0)), + "TicketAlertSeverity": reflect.ValueOf(model.TicketAlertSeverity), + "UnknownAlertSeverity": reflect.ValueOf(model.UnknownAlertSeverity), + + // type definitions + "AlertSeverity": reflect.ValueOf((*model.AlertSeverity)(nil)), + "Info": reflect.ValueOf((*model.Info)(nil)), + "MWMBAlert": reflect.ValueOf((*model.MWMBAlert)(nil)), + "MWMBAlertGroup": reflect.ValueOf((*model.MWMBAlertGroup)(nil)), + "Mode": reflect.ValueOf((*model.Mode)(nil)), + "PromAlertMeta": reflect.ValueOf((*model.PromAlertMeta)(nil)), + "PromRuleGroup": reflect.ValueOf((*model.PromRuleGroup)(nil)), + "PromSLI": reflect.ValueOf((*model.PromSLI)(nil)), + "PromSLIEvents": reflect.ValueOf((*model.PromSLIEvents)(nil)), + "PromSLIRaw": reflect.ValueOf((*model.PromSLIRaw)(nil)), + "PromSLO": reflect.ValueOf((*model.PromSLO)(nil)), + "PromSLOGroup": reflect.ValueOf((*model.PromSLOGroup)(nil)), + "PromSLOGroupSource": reflect.ValueOf((*model.PromSLOGroupSource)(nil)), + "PromSLORules": reflect.ValueOf((*model.PromSLORules)(nil)), + } +} diff --git a/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go b/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go new file mode 100644 index 00000000..b8f1a60f --- /dev/null +++ b/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go @@ -0,0 +1,15 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/utils/data'. DO NOT EDIT. + +package custom + +import ( + "github.com/slok/sloth/pkg/common/utils/data" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/common/utils/data/data"] = map[string]reflect.Value{ + // function, constant and variable definitions + "MergeLabels": reflect.ValueOf(data.MergeLabels), + } +} diff --git a/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go b/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go new file mode 100644 index 00000000..d9f81ad9 --- /dev/null +++ b/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go @@ -0,0 +1,16 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/utils/prometheus'. DO NOT EDIT. + +package custom + +import ( + "github.com/slok/sloth/pkg/common/utils/prometheus" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/common/utils/prometheus/prometheus"] = map[string]reflect.Value{ + // function, constant and variable definitions + "LabelsToPromFilter": reflect.ValueOf(prometheus.LabelsToPromFilter), + "TimeDurationToPromStr": reflect.ValueOf(prometheus.TimeDurationToPromStr), + } +} diff --git a/internal/plugin/slo/custom/github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go b/internal/plugin/slo/custom/github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go new file mode 100644 index 00000000..095b0621 --- /dev/null +++ b/internal/plugin/slo/custom/github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go @@ -0,0 +1,43 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/prometheus/plugin/slo/v1'. DO NOT EDIT. + +package custom + +import ( + "context" + "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + "go/constant" + "go/token" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/v1"] = map[string]reflect.Value{ + // function, constant and variable definitions + "PluginFactoryName": reflect.ValueOf(constant.MakeFromLiteral("\"NewPlugin\"", token.STRING, 0)), + "PluginIDName": reflect.ValueOf(constant.MakeFromLiteral("\"PluginID\"", token.STRING, 0)), + "PluginVersionName": reflect.ValueOf(constant.MakeFromLiteral("\"PluginVersion\"", token.STRING, 0)), + "Version": reflect.ValueOf(constant.MakeFromLiteral("\"prometheus/slo/v1\"", token.STRING, 0)), + + // type definitions + "AppUtils": reflect.ValueOf((*v1.AppUtils)(nil)), + "Plugin": reflect.ValueOf((*v1.Plugin)(nil)), + "PluginFactory": reflect.ValueOf((*v1.PluginFactory)(nil)), + "PluginID": reflect.ValueOf((*v1.PluginID)(nil)), + "PluginVersion": reflect.ValueOf((*v1.PluginVersion)(nil)), + "Request": reflect.ValueOf((*v1.Request)(nil)), + "Result": reflect.ValueOf((*v1.Result)(nil)), + + // interface wrapper definitions + "_Plugin": reflect.ValueOf((*_github_com_slok_sloth_pkg_prometheus_plugin_slo_v1_Plugin)(nil)), + } +} + +// _github_com_slok_sloth_pkg_prometheus_plugin_slo_v1_Plugin is an interface wrapper for Plugin type +type _github_com_slok_sloth_pkg_prometheus_plugin_slo_v1_Plugin struct { + IValue interface{} + WProcessSLO func(ctx context.Context, request *v1.Request, result *v1.Result) error +} + +func (W _github_com_slok_sloth_pkg_prometheus_plugin_slo_v1_Plugin) ProcessSLO(ctx context.Context, request *v1.Request, result *v1.Result) error { + return W.WProcessSLO(ctx, request, result) +} diff --git a/internal/plugin/slo/slo.go b/internal/plugin/slo/slo.go new file mode 100644 index 00000000..842979d6 --- /dev/null +++ b/internal/plugin/slo/slo.go @@ -0,0 +1,123 @@ +package slo + +import ( + "context" + "fmt" + "os" + "regexp" + + "github.com/traefik/yaegi/interp" + "github.com/traefik/yaegi/stdlib" + "github.com/traefik/yaegi/stdlib/unsafe" + + "github.com/slok/sloth/internal/plugin/slo/custom" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +type Plugin struct { + ID string + PluginFactory pluginslov1.PluginFactory +} + +// PluginLoader knows how to load Go SLO plugins using Yaegi. +const PluginLoader = pluginLoader(false) + +type pluginLoader bool + +var packageRegexp = regexp.MustCompile(`(?m)^package +([^\s]+) *$`) + +// LoadRawPlugin knows how to load plugins using Yaegi from source data not files, +// thats why, this implementation will not support any import library except standard +// library. +// +// The load process will search for: +// - A function called `NewPlugin` to obtain the plugin factory. +// - A constant called `PluginID` to obtain the plugin ID. +// - A constant called `PluginVersion` to obtain the plugin version. +func (p pluginLoader) LoadRawPlugin(ctx context.Context, src string) (*Plugin, error) { + // Load the plugin in a new interpreter. + // For each plugin we need to use an independent interpreter to avoid name collisions. + yaegiInterp, err := newYaeginInterpreter(false, false) + if err != nil { + return nil, fmt.Errorf("could not create a new Yaegi interpreter: %w", err) + } + + _, err = yaegiInterp.EvalWithContext(ctx, src) + if err != nil { + return nil, fmt.Errorf("could not evaluate plugin source code: %w", err) + } + + // Discover package name. + packageMatch := packageRegexp.FindStringSubmatch(src) + if len(packageMatch) != 2 { + return nil, fmt.Errorf("invalid plugin source code, could not get package name") + } + packageName := packageMatch[1] + + // Get plugin version and check if is a known one. + pluginVerTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.PluginVersion", packageName)) + if err != nil { + return nil, fmt.Errorf("could not get plugin version: %w", err) + } + + pluginVer, ok := pluginVerTmp.Interface().(pluginslov1.PluginVersion) + if !ok || (pluginVer != pluginslov1.Version) { + return nil, fmt.Errorf("unsuported plugin version: %s", pluginVer) + } + + // Get plugin ID. + pluginIDTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.PluginID", packageName)) + if err != nil { + return nil, fmt.Errorf("could not get plugin ID: %w", err) + } + + pluginID, ok := pluginIDTmp.Interface().(pluginslov1.PluginID) + if !ok || pluginID == "" { + return nil, fmt.Errorf("invalid SLO plugin ID type") + } + + // Get plugin logic. + pluginFuncTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.%s", packageName, pluginslov1.PluginFactoryName)) + if err != nil { + return nil, fmt.Errorf("could not get plugin: %w", err) + } + + plugin, ok := pluginFuncTmp.Interface().(pluginslov1.PluginFactory) + if !ok { + return nil, fmt.Errorf("invalid SLO plugin type") + } + + return &Plugin{ + ID: pluginID, + PluginFactory: plugin, + }, nil +} + +func newYaeginInterpreter(env, unrestricted bool) (*interp.Interpreter, error) { + envVars := []string{} + if env { + envVars = os.Environ() + } + i := interp.New(interp.Options{ + Env: envVars, + Unrestricted: unrestricted, + }) + err := i.Use(stdlib.Symbols) + if err != nil { + return nil, fmt.Errorf("could not use stdlib symbols: %w", err) + } + + // Add unsafe library. + err = i.Use(unsafe.Symbols) + if err != nil { + return nil, fmt.Errorf("yaegi could not use stdlib unsafe symbols: %w", err) + } + + // Add our own plugin library. + err = i.Use(custom.Symbols) + if err != nil { + return nil, fmt.Errorf("yaegi could not use custom symbols: %w", err) + } + + return i, nil +} diff --git a/internal/plugin/slo/slo_test.go b/internal/plugin/slo/slo_test.go new file mode 100644 index 00000000..4699741d --- /dev/null +++ b/internal/plugin/slo/slo_test.go @@ -0,0 +1,176 @@ +package slo_test + +import ( + "testing" + + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/slok/sloth/internal/log" + pluginslo "github.com/slok/sloth/internal/plugin/slo" + "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + pluginSrc string + execPlugin func(t *testing.T, p pluginslo.Plugin) + expErr bool + }{ + "Empty plugin should fail.": { + pluginSrc: "", + execPlugin: func(t *testing.T, p pluginslo.Plugin) {}, + expErr: true, + }, + + "An invalid plugin syntax should fail": { + pluginSrc: `package test{`, + execPlugin: func(t *testing.T, p pluginslo.Plugin) {}, + expErr: true, + }, + + "A plugin without the required version, should fail.": { + pluginSrc: `package test +import ( + "context" + "encoding/json" + + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v2" + PluginID = "sloth.dev/noop/v1" +) + +func NewPlugin(_ json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return noopPlugin{}, nil +} + +type noopPlugin struct{} + +func (noopPlugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + return nil +} +`, + execPlugin: func(t *testing.T, p pluginslo.Plugin) {}, + expErr: true, + }, + + "A plugin without the plugin ID, should fail.": { + pluginSrc: `package test +import ( + "context" + "encoding/json" + + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "" +) + +func NewPlugin(_ json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return noopPlugin{}, nil +} + +type noopPlugin struct{} + +func (noopPlugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + return nil +} +`, + execPlugin: func(t *testing.T, p pluginslo.Plugin) {}, + expErr: true, + }, + + "A plugin without the plugin factory, should fail.": { + pluginSrc: `package test +import ( + "context" + "encoding/json" + + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/noop/v1" +) + +func NewPlugin2(_ json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return noopPlugin{}, nil +} + +type noopPlugin struct{} + +func (noopPlugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + return nil +} +`, + execPlugin: func(t *testing.T, p pluginslo.Plugin) {}, + expErr: true, + }, + + "A correct plugin should execute the plugin.": { + pluginSrc: `package noopv1 + +import ( + "context" + "encoding/json" + + "github.com/prometheus/prometheus/model/rulefmt" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/test/v1" +) + +func NewPlugin(_ json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return test{}, nil +} + +type test struct{} + +func (test) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + result.SLORules.MetadataRecRules.Rules = []rulefmt.Rule{ + {Expr: "test1"}, + {Alert: "test2"}, + } + + return nil +} +`, + execPlugin: func(t *testing.T, p pluginslo.Plugin) { + plugin, err := p.PluginFactory(nil, pluginslov1.AppUtils{Logger: log.Noop}) + require.NoError(t, err) + gotResp := &pluginslov1.Result{} + err = plugin.ProcessSLO(t.Context(), &pluginslov1.Request{}, gotResp) + require.NoError(t, err) + expResp := pluginslov1.Result{SLORules: model.PromSLORules{MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + {Expr: "test1"}, + {Alert: "test2"}, + }}}, + } + assert.Equal(t, expResp, *gotResp) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + plugin, err := pluginslo.PluginLoader.LoadRawPlugin(t.Context(), test.pluginSrc) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + test.execPlugin(t, *plugin) + } + }) + } +} diff --git a/internal/prometheus/model.go b/internal/prometheus/model.go deleted file mode 100644 index 140895d6..00000000 --- a/internal/prometheus/model.go +++ /dev/null @@ -1,14 +0,0 @@ -package prometheus - -import ( - "github.com/slok/sloth/pkg/common/model" -) - -// TODO(slok): Remove after migration to pkg/common/model package. -type SLO = model.PromSLO -type SLI = model.PromSLI -type SLIEvents = model.PromSLIEvents -type AlertMeta = model.PromAlertMeta -type SLOGroup = model.PromSLOGroup -type SLORules = model.PromSLORules -type SLIRaw = model.PromSLIRaw diff --git a/internal/storage/io/k8s_sloth.go b/internal/storage/io/k8s_sloth.go index 154fd7f6..008e9676 100644 --- a/internal/storage/io/k8s_sloth.go +++ b/internal/storage/io/k8s_sloth.go @@ -8,7 +8,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" - "github.com/slok/sloth/internal/prometheus" "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" k8sprometheusv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" @@ -87,10 +86,10 @@ func (l K8sSlothPrometheusYAMLSpecLoader) LoadSpec(ctx context.Context, data []b } func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, pluginsRepo SLIPluginRepo, kspec *k8sprometheusv1.PrometheusServiceLevel) (*model.PromSLOGroup, error) { - slos := make([]prometheus.SLO, 0, len(kspec.Spec.SLOs)) + slos := make([]model.PromSLO, 0, len(kspec.Spec.SLOs)) spec := kspec.Spec for _, specSLO := range kspec.Spec.SLOs { - slo := prometheus.SLO{ + slo := model.PromSLO{ ID: fmt.Sprintf("%s-%s", spec.Service, specSLO.Name), Name: specSLO.Name, Description: specSLO.Description, @@ -98,20 +97,20 @@ func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, plug TimeWindow: defaultWindowPeriod, Objective: specSLO.Objective, Labels: utilsdata.MergeLabels(spec.Labels, specSLO.Labels), - PageAlertMeta: prometheus.AlertMeta{Disable: true}, - TicketAlertMeta: prometheus.AlertMeta{Disable: true}, + PageAlertMeta: model.PromAlertMeta{Disable: true}, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, } // Set SLIs. if specSLO.SLI.Events != nil { - slo.SLI.Events = &prometheus.SLIEvents{ + slo.SLI.Events = &model.PromSLIEvents{ ErrorQuery: specSLO.SLI.Events.ErrorQuery, TotalQuery: specSLO.SLI.Events.TotalQuery, } } if specSLO.SLI.Raw != nil { - slo.SLI.Raw = &prometheus.SLIRaw{ + slo.SLI.Raw = &model.PromSLIRaw{ ErrorRatioQuery: specSLO.SLI.Raw.ErrorRatioQuery, } } @@ -133,14 +132,14 @@ func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, plug return nil, fmt.Errorf("plugin %q execution error: %w", specSLO.SLI.Plugin.ID, err) } - slo.SLI.Raw = &prometheus.SLIRaw{ + slo.SLI.Raw = &model.PromSLIRaw{ ErrorRatioQuery: rawQuery, } } // Set alerts. if !specSLO.Alerting.PageAlert.Disable { - slo.PageAlertMeta = prometheus.AlertMeta{ + slo.PageAlertMeta = model.PromAlertMeta{ Name: specSLO.Alerting.Name, Labels: utilsdata.MergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.PageAlert.Labels), Annotations: utilsdata.MergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.PageAlert.Annotations), @@ -148,7 +147,7 @@ func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, plug } if !specSLO.Alerting.TicketAlert.Disable { - slo.TicketAlertMeta = prometheus.AlertMeta{ + slo.TicketAlertMeta = model.PromAlertMeta{ Name: specSLO.Alerting.Name, Labels: utilsdata.MergeLabels(specSLO.Alerting.Labels, specSLO.Alerting.TicketAlert.Labels), Annotations: utilsdata.MergeLabels(specSLO.Alerting.Annotations, specSLO.Alerting.TicketAlert.Annotations), diff --git a/internal/storage/io/k8s_sloth_test.go b/internal/storage/io/k8s_sloth_test.go index 70bb84c0..c1230446 100644 --- a/internal/storage/io/k8s_sloth_test.go +++ b/internal/storage/io/k8s_sloth_test.go @@ -10,7 +10,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" pluginsli "github.com/slok/sloth/internal/plugin/sli" - "github.com/slok/sloth/internal/prometheus" "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" kubeslothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" @@ -147,21 +146,21 @@ spec: ticketAlert: disable: true `, - expModel: &model.PromSLOGroup{SLOs: []prometheus.SLO{ + expModel: &model.PromSLOGroup{SLOs: []model.PromSLO{ { ID: "test-svc-slo-test", Name: "slo-test", Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, Labels: map[string]string{"gk1": "gv1"}, - SLI: prometheus.SLI{ - Raw: &prometheus.SLIRaw{ + SLI: model.PromSLI{ + Raw: &model.PromSLIRaw{ ErrorRatioQuery: `plugin_raw_expr{service="test-svc",slo="slo-test",objective="99.000000",gk1="gv1",k1="v1",k2="true"}`, }, }, Objective: 99, - PageAlertMeta: prometheus.AlertMeta{Disable: true}, - TicketAlertMeta: prometheus.AlertMeta{Disable: true}, + PageAlertMeta: model.PromAlertMeta{Disable: true}, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, }, }, OriginalSource: model.PromSLOGroupSource{K8sSlothV1: &kubeslothv1.PrometheusServiceLevel{ @@ -280,15 +279,15 @@ spec: ticketAlert: disable: true `, - expModel: &model.PromSLOGroup{SLOs: []prometheus.SLO{ + expModel: &model.PromSLOGroup{SLOs: []model.PromSLO{ { ID: "test-svc-slo1", Name: "slo1", Description: "This is a test.", Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ ErrorQuery: "test_expr_error_1", TotalQuery: "test_expr_total_1", }, @@ -298,7 +297,7 @@ spec: "owner": "myteam", "category": "test", }, - PageAlertMeta: prometheus.AlertMeta{ + PageAlertMeta: model.PromAlertMeta{ Disable: false, Name: "testAlert", Labels: map[string]string{ @@ -311,7 +310,7 @@ spec: "runbook": "http://whatever.com", }, }, - TicketAlertMeta: prometheus.AlertMeta{ + TicketAlertMeta: model.PromAlertMeta{ Disable: false, Name: "testAlert", Labels: map[string]string{ @@ -330,8 +329,8 @@ spec: Name: "slo2", Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Raw: &prometheus.SLIRaw{ + SLI: model.PromSLI{ + Raw: &model.PromSLIRaw{ ErrorRatioQuery: "test_expr_ratio_2", }, }, @@ -340,8 +339,8 @@ spec: "owner": "myteam", "category": "test2", }, - PageAlertMeta: prometheus.AlertMeta{Disable: true}, - TicketAlertMeta: prometheus.AlertMeta{Disable: true}, + PageAlertMeta: model.PromAlertMeta{Disable: true}, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, }, }, OriginalSource: model.PromSLOGroupSource{K8sSlothV1: &kubeslothv1.PrometheusServiceLevel{ diff --git a/internal/storage/io/openslo_test.go b/internal/storage/io/openslo_test.go index 10452888..bf7173ec 100644 --- a/internal/storage/io/openslo_test.go +++ b/internal/storage/io/openslo_test.go @@ -9,7 +9,6 @@ import ( "github.com/OpenSLO/oslo/pkg/manifest/v1alpha" "github.com/stretchr/testify/assert" - "github.com/slok/sloth/internal/prometheus" "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" ) @@ -17,7 +16,7 @@ import ( func TestOpenSLOYAMLSpecLoader(t *testing.T) { tests := map[string]struct { specYaml string - expModel *prometheus.SLOGroup + expModel *model.PromSLOGroup expErr bool }{ "Empty spec should fail.": { @@ -221,15 +220,15 @@ spec: isRolling: true unit: Day `, - expModel: &prometheus.SLOGroup{SLOs: []prometheus.SLO{ + expModel: &model.PromSLOGroup{SLOs: []model.PromSLO{ { ID: "my-test-service-ratio-0", Name: "ratio-0", Service: "my-test-service", Description: "A great description of a ratio based SLO", TimeWindow: 28 * 24 * time.Hour, - SLI: prometheus.SLI{ - Raw: &prometheus.SLIRaw{ + SLI: model.PromSLI{ + Raw: &model.PromSLIRaw{ ErrorRatioQuery: ` 1 - ( ( @@ -244,8 +243,8 @@ spec: }, }, Objective: 98, - PageAlertMeta: prometheus.AlertMeta{Disable: true}, - TicketAlertMeta: prometheus.AlertMeta{Disable: true}, + PageAlertMeta: model.PromAlertMeta{Disable: true}, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, }, { ID: "my-test-service-ratio-1", @@ -253,8 +252,8 @@ spec: Service: "my-test-service", Description: "A great description of a ratio based SLO", TimeWindow: 28 * 24 * time.Hour, - SLI: prometheus.SLI{ - Raw: &prometheus.SLIRaw{ + SLI: model.PromSLI{ + Raw: &model.PromSLIRaw{ ErrorRatioQuery: ` 1 - ( ( @@ -269,8 +268,8 @@ spec: }, }, Objective: 99.9, - PageAlertMeta: prometheus.AlertMeta{Disable: true}, - TicketAlertMeta: prometheus.AlertMeta{Disable: true}, + PageAlertMeta: model.PromAlertMeta{Disable: true}, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, }, }, OriginalSource: model.PromSLOGroupSource{OpenSLOV1Alpha: &v1alpha.SLO{ diff --git a/internal/storage/io/prometheus_operator_test.go b/internal/storage/io/prometheus_operator_test.go index 7f2ccdff..8c4ebd45 100644 --- a/internal/storage/io/prometheus_operator_test.go +++ b/internal/storage/io/prometheus_operator_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/prometheus" "github.com/slok/sloth/internal/storage" "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" @@ -45,8 +44,8 @@ func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { }, slos: []storage.SLORulesResult{ { - SLO: prometheus.SLO{ID: "test1"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "test1"}, + Rules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record", @@ -94,8 +93,8 @@ spec: }, slos: []storage.SLORulesResult{ { - SLO: prometheus.SLO{ID: "test1"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "test1"}, + Rules: model.PromSLORules{ MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record", @@ -143,8 +142,8 @@ spec: }, slos: []storage.SLORulesResult{ { - SLO: prometheus.SLO{ID: "test1"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "test1"}, + Rules: model.PromSLORules{ AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Alert: "testAlert", @@ -196,8 +195,8 @@ spec: slos: []storage.SLORulesResult{ { - SLO: prometheus.SLO{ID: "testa"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "testa"}, + Rules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a1", @@ -239,8 +238,8 @@ spec: }, }, { - SLO: prometheus.SLO{ID: "testb"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "testb"}, + Rules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b1", diff --git a/internal/storage/io/sloth_test.go b/internal/storage/io/sloth_test.go index 876bb9cc..5d6b5294 100644 --- a/internal/storage/io/sloth_test.go +++ b/internal/storage/io/sloth_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/assert" pluginsli "github.com/slok/sloth/internal/plugin/sli" - "github.com/slok/sloth/internal/prometheus" "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" v1 "github.com/slok/sloth/pkg/prometheus/api/v1" @@ -30,7 +29,7 @@ func TestSlothPrometheusYAMLSpecLoader(t *testing.T) { specYaml string plugins map[string]pluginsli.SLIPlugin windowPeriod time.Duration - expModel *prometheus.SLOGroup + expModel *model.PromSLOGroup expErr bool }{ "Empty spec should fail.": { @@ -153,21 +152,21 @@ slos: ticket_alert: disable: true `, - expModel: &prometheus.SLOGroup{SLOs: []prometheus.SLO{ + expModel: &model.PromSLOGroup{SLOs: []model.PromSLO{ { ID: "test-svc-slo-test", Name: "slo-test", Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, Labels: map[string]string{"gk1": "gv1"}, - SLI: prometheus.SLI{ - Raw: &prometheus.SLIRaw{ + SLI: model.PromSLI{ + Raw: &model.PromSLIRaw{ ErrorRatioQuery: `plugin_raw_expr{service="test-svc",slo="slo-test",objective="99.000000",gk1="gv1",k1="v1",k2="true"}`, }, }, Objective: 99, - PageAlertMeta: prometheus.AlertMeta{Disable: true}, - TicketAlertMeta: prometheus.AlertMeta{Disable: true}, + PageAlertMeta: model.PromAlertMeta{Disable: true}, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, }, }, OriginalSource: model.PromSLOGroupSource{SlothV1: &v1.Spec{ @@ -211,21 +210,21 @@ slos: ticket_alert: disable: true `, - expModel: &prometheus.SLOGroup{SLOs: []prometheus.SLO{ + expModel: &model.PromSLOGroup{SLOs: []model.PromSLO{ { ID: "test-svc-slo-test", Name: "slo-test", Service: "test-svc", TimeWindow: 28 * 24 * time.Hour, Labels: map[string]string{"gk1": "gv1"}, - SLI: prometheus.SLI{ - Raw: &prometheus.SLIRaw{ + SLI: model.PromSLI{ + Raw: &model.PromSLIRaw{ ErrorRatioQuery: `test_expr_ratio_2`, }, }, Objective: 99, - PageAlertMeta: prometheus.AlertMeta{Disable: true}, - TicketAlertMeta: prometheus.AlertMeta{Disable: true}, + PageAlertMeta: model.PromAlertMeta{Disable: true}, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, }, }, OriginalSource: model.PromSLOGroupSource{SlothV1: &v1.Spec{ @@ -295,15 +294,15 @@ slos: ticket_alert: disable: true `, - expModel: &prometheus.SLOGroup{SLOs: []prometheus.SLO{ + expModel: &model.PromSLOGroup{SLOs: []model.PromSLO{ { ID: "test-svc-slo1", Name: "slo1", Service: "test-svc", Description: "This is a test.", TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Events: &prometheus.SLIEvents{ + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ ErrorQuery: "test_expr_error_1", TotalQuery: "test_expr_total_1", }, @@ -313,7 +312,7 @@ slos: "owner": "myteam", "category": "test", }, - PageAlertMeta: prometheus.AlertMeta{ + PageAlertMeta: model.PromAlertMeta{ Disable: false, Name: "testAlert", Labels: map[string]string{ @@ -326,7 +325,7 @@ slos: "runbook": "http://whatever.com", }, }, - TicketAlertMeta: prometheus.AlertMeta{ + TicketAlertMeta: model.PromAlertMeta{ Disable: false, Name: "testAlert", Labels: map[string]string{ @@ -345,8 +344,8 @@ slos: Name: "slo2", Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, - SLI: prometheus.SLI{ - Raw: &prometheus.SLIRaw{ + SLI: model.PromSLI{ + Raw: &model.PromSLIRaw{ ErrorRatioQuery: "test_expr_ratio_2", }, }, @@ -355,8 +354,8 @@ slos: "owner": "myteam", "category": "test2", }, - PageAlertMeta: prometheus.AlertMeta{Disable: true}, - TicketAlertMeta: prometheus.AlertMeta{Disable: true}, + PageAlertMeta: model.PromAlertMeta{Disable: true}, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, }, }, OriginalSource: model.PromSLOGroupSource{SlothV1: &v1.Spec{ diff --git a/internal/storage/k8s/k8s_test.go b/internal/storage/k8s/k8s_test.go index fa354094..a134a04c 100644 --- a/internal/storage/k8s/k8s_test.go +++ b/internal/storage/k8s/k8s_test.go @@ -15,7 +15,6 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/prometheus" "github.com/slok/sloth/internal/storage" storagek8s "github.com/slok/sloth/internal/storage/k8s" "github.com/slok/sloth/pkg/common/model" @@ -55,8 +54,8 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { }, slos: []storage.SLORulesResult{ { - SLO: prometheus.SLO{ID: "testa"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "testa"}, + Rules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-a1", @@ -98,8 +97,8 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { }, }, { - SLO: prometheus.SLO{ID: "testb"}, - Rules: prometheus.SLORules{ + SLO: model.PromSLO{ID: "testb"}, + Rules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ { Record: "test:record-b1", diff --git a/pkg/common/utils/data/data.go b/pkg/common/utils/data/data.go index 85ccaa6c..886cfff8 100644 --- a/pkg/common/utils/data/data.go +++ b/pkg/common/utils/data/data.go @@ -2,10 +2,22 @@ package data import "maps" -func MergeLabels[M ~map[K]V, K comparable, V any](ms ...M) M { +func MergeMaps[M ~map[K]V, K comparable, V any](ms ...M) M { m := make(M) for _, m2 := range ms { maps.Copy(m, m2) } return m } + +type mss = map[string]string + +func MergeLabels(ms ...mss) mss { + res := mss{} + for _, m := range ms { + for k, v := range m { + res[k] = v + } + } + return res +} diff --git a/pkg/prometheus/plugin/slo/v1/testing/testing.go b/pkg/prometheus/plugin/slo/v1/testing/testing.go new file mode 100644 index 00000000..486a34d0 --- /dev/null +++ b/pkg/prometheus/plugin/slo/v1/testing/testing.go @@ -0,0 +1,56 @@ +package testing + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/slok/sloth/internal/log" + pluginslo "github.com/slok/sloth/internal/plugin/slo" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +type TestPluginConfig struct { + PluginFilePath string + PluginConfiguration json.RawMessage +} + +func (c *TestPluginConfig) defaults() error { + if c.PluginFilePath == "" { + c.PluginFilePath = "./plugin.go" + } + + if c.PluginConfiguration == nil { + c.PluginConfiguration = []byte("{}") + } + + return nil +} + +// NewTestPlugin is a helper util to load a plugin using the engine that +// will use Sloth. In the sense of an acceptance/integration test. +// +// This has benefits over loading the plugin directly with Go, by using this method +// you will be sure that what is executed is what the sloth will execute at runtime, +// so, if you use a not supported feature or the engine has a bug, this will be +// detected on the tests instead of Sloth runtime on execution. +func NewTestPlugin(ctx context.Context, config TestPluginConfig) (pluginslov1.Plugin, error) { + err := config.defaults() + if err != nil { + return nil, fmt.Errorf("invalid configuration: %w", err) + } + + pluginSource, err := os.ReadFile(config.PluginFilePath) + if err != nil { + return nil, fmt.Errorf("could not read plugin source code: %w", err) + } + plugin, err := pluginslo.PluginLoader.LoadRawPlugin(ctx, string(pluginSource)) + if err != nil { + return nil, fmt.Errorf("could not load plugin source code: %w", err) + } + + return plugin.PluginFactory(config.PluginConfiguration, pluginslov1.AppUtils{ + Logger: log.Noop, + }) +} diff --git a/pkg/prometheus/plugin/slo/v1/v1.go b/pkg/prometheus/plugin/slo/v1/v1.go new file mode 100644 index 00000000..2881f81b --- /dev/null +++ b/pkg/prometheus/plugin/slo/v1/v1.go @@ -0,0 +1,54 @@ +package v1 + +import ( + "context" + "encoding/json" + + "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/pkg/common/model" +) + +// Version is this plugin type version. +const Version = "prometheus/slo/v1" + +// PluginVersion is the version of the plugin (e.g: `prometheus/slo/v1`). +type PluginVersion = string + +const PluginVersionName = "PluginVersion" + +// PluginID is the ID of the plugin (e.g: sloth.dev/my-test-plugin/v1). +type PluginID = string + +const PluginIDName = "PluginID" + +// AppUtils are app utils plugins can use in their logic. +type AppUtils struct { + Logger log.Logger +} + +type Request struct { + // Info about the application and execution, normally used as metadata. + Info model.Info + // The SLO to process and generate the final Prom rules. + SLO model.PromSLO + // The SLO MWMBAlertGroup selected. + MWMBAlertGroup model.MWMBAlertGroup +} + +type Result struct { + SLORules model.PromSLORules +} + +// PluginFactoryName is the required name for the plugin factory. +const PluginFactoryName = "NewPlugin" + +type PluginFactory = func(config json.RawMessage, appUtils AppUtils) (Plugin, error) + +// Plugin knows how to process SLOs in a chain of plugins. +// * The plugin processor can change the result argument of the SLO processing with the resulting prometheus rules. +// * The plugin processor can also modify the request object, but this is not recommended as it can lead to unexpected behavior. +// +// This is the type the SLO plugins need to implement. +type Plugin interface { + ProcessSLO(ctx context.Context, request *Request, result *Result) error +} From e1982108405794399e7a7279d08dd88f9cbd9d78 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Fri, 4 Apr 2025 08:38:59 +0200 Subject: [PATCH 036/173] Add intervals to prom rule models Signed-off-by: Xabier Larrakoetxea --- internal/kubernetes/modelmap/slo.go | 26 ++++++++++++++----- .../storage/io/prometheus_operator_test.go | 20 ++++++++------ internal/storage/io/std_prometheus.go | 15 ++++++----- internal/storage/io/std_prometheus_test.go | 20 ++++++++------ internal/storage/k8s/k8s.go | 1 - pkg/common/model/slo_prometheus.go | 3 ++- 6 files changed, 55 insertions(+), 30 deletions(-) diff --git a/internal/kubernetes/modelmap/slo.go b/internal/kubernetes/modelmap/slo.go index 399e3083..fa2e7a11 100644 --- a/internal/kubernetes/modelmap/slo.go +++ b/internal/kubernetes/modelmap/slo.go @@ -3,6 +3,7 @@ package modelmap import ( "context" "fmt" + "time" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" "github.com/prometheus/prometheus/model/rulefmt" @@ -11,6 +12,7 @@ import ( "github.com/slok/sloth/internal/storage" commonerrors "github.com/slok/sloth/pkg/common/errors" + promutils "github.com/slok/sloth/pkg/common/utils/prometheus" ) func MapModelToPrometheusOperator(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) (*monitoringv1.PrometheusRule, error) { @@ -43,22 +45,25 @@ func MapModelToPrometheusOperator(ctx context.Context, kmeta storage.K8sMeta, sl for _, slo := range slos { if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.SLIErrorRecRules.Rules), + Interval: timeDurationToPromOpDuration(slo.Rules.SLIErrorRecRules.Interval), + Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), + Rules: promRulesToKubeRules(slo.Rules.SLIErrorRecRules.Rules), }) } if len(slo.Rules.MetadataRecRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.MetadataRecRules.Rules), + Interval: timeDurationToPromOpDuration(slo.Rules.MetadataRecRules.Interval), + Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), + Rules: promRulesToKubeRules(slo.Rules.MetadataRecRules.Rules), }) } if len(slo.Rules.AlertRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), - Rules: promRulesToKubeRules(slo.Rules.AlertRules.Rules), + Interval: timeDurationToPromOpDuration(slo.Rules.AlertRules.Interval), + Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), + Rules: promRulesToKubeRules(slo.Rules.AlertRules.Rules), }) } } @@ -97,3 +102,12 @@ func promRulesToKubeRules(rules []rulefmt.Rule) []monitoringv1.Rule { } return res } + +func timeDurationToPromOpDuration(t time.Duration) *monitoringv1.Duration { + if t == 0 { + return nil + } + + r := monitoringv1.Duration(promutils.TimeDurationToPromStr(t)) + return &r +} diff --git a/internal/storage/io/prometheus_operator_test.go b/internal/storage/io/prometheus_operator_test.go index 8c4ebd45..bb27b0ed 100644 --- a/internal/storage/io/prometheus_operator_test.go +++ b/internal/storage/io/prometheus_operator_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "testing" + "time" "github.com/prometheus/prometheus/model/rulefmt" "github.com/stretchr/testify/assert" @@ -95,13 +96,15 @@ spec: { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - }, - }}, + MetadataRecRules: model.PromRuleGroup{ + Interval: 42 * time.Minute, + Rules: []rulefmt.Rule{ + { + Record: "test:record", + Expr: "test-expr", + Labels: map[string]string{"test-label": "one"}, + }, + }}, }, }, }, @@ -124,7 +127,8 @@ metadata: namespace: test-ns spec: groups: - - name: sloth-slo-meta-recordings-test1 + - interval: 42m + name: sloth-slo-meta-recordings-test1 rules: - expr: test-expr labels: diff --git a/internal/storage/io/std_prometheus.go b/internal/storage/io/std_prometheus.go index 9fefde55..c38deb46 100644 --- a/internal/storage/io/std_prometheus.go +++ b/internal/storage/io/std_prometheus.go @@ -50,22 +50,25 @@ func (r StdPrometheusGroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos [ for _, slo := range slos { if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ - Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), - Rules: slo.Rules.SLIErrorRecRules.Rules, + Interval: prommodel.Duration(slo.Rules.SLIErrorRecRules.Interval), + Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), + Rules: slo.Rules.SLIErrorRecRules.Rules, }) } if len(slo.Rules.MetadataRecRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ - Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), - Rules: slo.Rules.MetadataRecRules.Rules, + Interval: prommodel.Duration(slo.Rules.MetadataRecRules.Interval), + Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), + Rules: slo.Rules.MetadataRecRules.Rules, }) } if len(slo.Rules.AlertRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ - Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), - Rules: slo.Rules.AlertRules.Rules, + Interval: prommodel.Duration(slo.Rules.AlertRules.Interval), + Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), + Rules: slo.Rules.AlertRules.Rules, }) } } diff --git a/internal/storage/io/std_prometheus_test.go b/internal/storage/io/std_prometheus_test.go index daddbe4e..09439d71 100644 --- a/internal/storage/io/std_prometheus_test.go +++ b/internal/storage/io/std_prometheus_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "testing" + "time" "github.com/prometheus/prometheus/model/rulefmt" "github.com/stretchr/testify/assert" @@ -94,14 +95,16 @@ groups: { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlert", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - Annotations: map[string]string{"test-annot": "one"}, - }, - }}, + AlertRules: model.PromRuleGroup{ + Interval: 42 * time.Minute, + Rules: []rulefmt.Rule{ + { + Alert: "testAlert", + Expr: "test-expr", + Labels: map[string]string{"test-label": "one"}, + Annotations: map[string]string{"test-annot": "one"}, + }, + }}, }, }, }, @@ -112,6 +115,7 @@ groups: groups: - name: sloth-slo-alerts-test1 + interval: 42m rules: - alert: testAlert expr: test-expr diff --git a/internal/storage/k8s/k8s.go b/internal/storage/k8s/k8s.go index 0e0ed21c..6e4affb6 100644 --- a/internal/storage/k8s/k8s.go +++ b/internal/storage/k8s/k8s.go @@ -70,7 +70,6 @@ func (r ApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMet if err != nil { return fmt.Errorf("could not map model to Prometheus operator CR: %w", err) } - fmt.Println(rule) // Add object reference. rule.ObjectMeta.OwnerReferences = append(rule.ObjectMeta.OwnerReferences, metav1.OwnerReference{ diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index af76d032..43d1ef43 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -289,5 +289,6 @@ type PromSLORules struct { // PromRuleGroup are regular prometheus group of rules. type PromRuleGroup struct { - Rules []rulefmt.Rule + Interval time.Duration + Rules []rulefmt.Rule } From b5a817debc410fdeca0dc259a1b64f4ba7f09d30 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 5 Apr 2025 07:55:11 +0200 Subject: [PATCH 037/173] Add SLO plugin and SLO plugin repository on domain layer Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 1 + cmd/sloth/commands/generate.go | 17 +- cmd/sloth/commands/helpers.go | 25 ++- cmd/sloth/commands/k8scontroller.go | 18 +- cmd/sloth/commands/validate.go | 20 +- internal/app/generate/generate.go | 48 ++++- .../generatemock/slo_plugin_getter.go | 60 ++++++ internal/plugin/plugin.go | 9 + internal/plugin/slo/core/noop_v1/plugin.go | 6 +- internal/{plugin => pluginengine}/sli/sli.go | 0 .../{plugin => pluginengine}/sli/sli_test.go | 2 +- .../slo/custom/custom.go | 0 .../github_com-prometheus-common-model.go | 0 ...com-prometheus-prometheus-model-rulefmt.go | 0 ...com-prometheus-prometheus-promql-parser.go | 0 ...b_com-slok-sloth-pkg-common-conventions.go | 0 .../github_com-slok-sloth-pkg-common-model.go | 30 +-- ...ub_com-slok-sloth-pkg-common-utils-data.go | 0 ...-slok-sloth-pkg-common-utils-prometheus.go | 0 ...slok-sloth-pkg-prometheus-plugin-slo-v1.go | 0 internal/{plugin => pluginengine}/slo/slo.go | 10 +- .../{plugin => pluginengine}/slo/slo_test.go | 20 +- .../storage/fs/fsmock/sli_plugin_loader.go | 2 +- .../storage/fs/fsmock/slo_plugin_loader.go | 60 ++++++ internal/storage/fs/sli_plugin.go | 14 +- internal/storage/fs/sli_plugin_test.go | 28 +-- internal/storage/fs/slo_plugin.go | 124 ++++++++++++ internal/storage/fs/slo_plugin_test.go | 178 ++++++++++++++++++ internal/storage/io/k8s_sloth_test.go | 10 +- internal/storage/io/sloth.go | 4 +- internal/storage/io/sloth_test.go | 14 +- pkg/common/errors/errors.go | 3 + pkg/common/model/slo_prometheus.go | 10 + .../plugin/slo/v1/testing/testing.go | 6 +- 34 files changed, 627 insertions(+), 92 deletions(-) create mode 100644 internal/app/generate/generatemock/slo_plugin_getter.go create mode 100644 internal/plugin/plugin.go rename internal/{plugin => pluginengine}/sli/sli.go (100%) rename internal/{plugin => pluginengine}/sli/sli_test.go (98%) rename internal/{plugin => pluginengine}/slo/custom/custom.go (100%) rename internal/{plugin => pluginengine}/slo/custom/github_com-prometheus-common-model.go (100%) rename internal/{plugin => pluginengine}/slo/custom/github_com-prometheus-prometheus-model-rulefmt.go (100%) rename internal/{plugin => pluginengine}/slo/custom/github_com-prometheus-prometheus-promql-parser.go (100%) rename internal/{plugin => pluginengine}/slo/custom/github_com-slok-sloth-pkg-common-conventions.go (100%) rename internal/{plugin => pluginengine}/slo/custom/github_com-slok-sloth-pkg-common-model.go (53%) rename internal/{plugin => pluginengine}/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go (100%) rename internal/{plugin => pluginengine}/slo/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go (100%) rename internal/{plugin => pluginengine}/slo/custom/github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go (100%) rename internal/{plugin => pluginengine}/slo/slo.go (95%) rename internal/{plugin => pluginengine}/slo/slo_test.go (84%) create mode 100644 internal/storage/fs/fsmock/slo_plugin_loader.go create mode 100644 internal/storage/fs/slo_plugin.go create mode 100644 internal/storage/fs/slo_plugin_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 425b261d..4d15ad7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Sloth domain models can be imported in Go apps using `github.com/slok/sloth/pkg/common/model`. - Sloth conventions can be imported in Go apps using `github.com/slok/sloth/pkg/common/conventions`. - A new SLO rule generation plugin system has been added to be able to change/extend the SLO rule generation process. +- SLO plugins can be loaded from FS directories recursively using `--slo-plugins-path` in the commands. ## [v0.12.0] - 2025-03-27 diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index c5f6f4c4..981210ee 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -24,6 +24,7 @@ import ( plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" "github.com/slok/sloth/internal/storage" + storagefs "github.com/slok/sloth/internal/storage/fs" storageio "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" kubernetesv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" @@ -40,6 +41,7 @@ type generateCommand struct { disableOptimizedRules bool extraLabels map[string]string sliPluginsPaths []string + sloPluginsPaths []string sloPeriodWindowsPath string sloPeriod string } @@ -57,6 +59,7 @@ func NewGenerateCommand(app *kingpin.Application) Command { cmd.Flag("disable-recordings", "Disables recording rules generation.").BoolVar(&c.disableRecordings) cmd.Flag("disable-alerts", "Disables alert rules generation.").BoolVar(&c.disableAlerts) cmd.Flag("sli-plugins-path", "The path to SLI plugins (can be repeated), if not set it disable plugins support.").Short('p').StringsVar(&c.sliPluginsPaths) + cmd.Flag("slo-plugins-path", "The path to SLO plugins a.k.a SLO generation middlewares (can be repeated).").Short('m').StringsVar(&c.sloPluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) cmd.Flag("disable-optimized-rules", "If enabled it will disable optimized generated rules.").BoolVar(&c.disableOptimizedRules) @@ -109,7 +112,12 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { }) // Load plugins - pluginRepo, err := createPluginLoader(ctx, logger, g.sliPluginsPaths) + pluginSLIRepo, err := createSLIPluginLoader(ctx, logger, g.sliPluginsPaths) + if err != nil { + return err + } + + pluginSLORepo, err := createSLOPluginLoader(ctx, logger, g.sloPluginsPaths) if err != nil { return err } @@ -134,8 +142,8 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { } // Create Spec loaders. - promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) - kubeYAMLLoader := storageio.NewK8sSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) + promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginSLIRepo, sloPeriod) + kubeYAMLLoader := storageio.NewK8sSlothPrometheusYAMLSpecLoader(pluginSLIRepo, sloPeriod) openSLOYAMLLoader := storageio.NewOpenSLOYAMLSpecLoader(sloPeriod) // Get SLO targets. @@ -248,6 +256,7 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { disableAlerts: g.disableAlerts, disableOptimizedRules: g.disableOptimizedRules, extraLabels: g.extraLabels, + sloPluginRepo: pluginSLORepo, } for _, genTarget := range genTargets { @@ -308,6 +317,7 @@ type generator struct { disableAlerts bool disableOptimizedRules bool extraLabels map[string]string + sloPluginRepo *storagefs.FileSLOPluginRepo } // GeneratePrometheus generates the SLOs based on a raw regular Prometheus spec format input and outs a Prometheus raw yaml. @@ -460,6 +470,7 @@ func (g generator) generateRules(ctx context.Context, info model.Info, slos mode SLIRulesGenSLOPlugin: sliRuleGen, MetadataRulesGenSLOPlugin: metaRuleGen, AlertRulesGenSLOPlugin: alertRuleGen, + SLOPluginGetter: g.sloPluginRepo, Logger: g.logger, }) if err != nil { diff --git a/cmd/sloth/commands/helpers.go b/cmd/sloth/commands/helpers.go index f2f99e60..c0b4c49e 100644 --- a/cmd/sloth/commands/helpers.go +++ b/cmd/sloth/commands/helpers.go @@ -5,12 +5,15 @@ import ( "context" "fmt" "io/fs" + "os" "path/filepath" "regexp" "strings" "github.com/slok/sloth/internal/log" - pluginsli "github.com/slok/sloth/internal/plugin/sli" + "github.com/slok/sloth/internal/plugin" + pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" + pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" storagefs "github.com/slok/sloth/internal/storage/fs" ) @@ -39,10 +42,10 @@ func splitYAML(data []byte) []string { return nonEmptyData } -func createPluginLoader(ctx context.Context, logger log.Logger, paths []string) (*storagefs.FileSLIPluginRepo, error) { +func createSLIPluginLoader(ctx context.Context, logger log.Logger, paths []string) (*storagefs.FileSLIPluginRepo, error) { config := storagefs.FileSLIPluginRepoConfig{ Paths: paths, - PluginLoader: pluginsli.PluginLoader, + PluginLoader: pluginenginesli.PluginLoader, Logger: logger, } sliPluginRepo, err := storagefs.NewFileSLIPluginRepo(config) @@ -53,6 +56,22 @@ func createPluginLoader(ctx context.Context, logger log.Logger, paths []string) return sliPluginRepo, nil } +func createSLOPluginLoader(ctx context.Context, logger log.Logger, paths []string) (*storagefs.FileSLOPluginRepo, error) { + fss := []fs.FS{ + plugin.EmbeddedDefaultSLOPlugins, + } + for _, p := range paths { + fss = append(fss, os.DirFS(p)) + } + + sliPluginRepo, err := storagefs.NewFileSLOPluginRepo(logger, pluginengineslo.PluginLoader, fss...) + if err != nil { + return nil, fmt.Errorf("could not create file SLO plugin repository: %w", err) + } + + return sliPluginRepo, nil +} + func discoverSLOManifests(logger log.Logger, exclude, include *regexp.Regexp, path string) ([]string, error) { logger = logger.WithValues(log.Kv{"svc": "SLODiscovery"}) diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index fc6fbe30..3f101798 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -69,6 +69,7 @@ type kubeControllerCommand struct { hotReloadAddr string metricsListenAddr string sliPluginsPaths []string + sloPluginsPaths []string sloPeriodWindowsPath string sloPeriod string disableOptimizedRules bool @@ -96,6 +97,7 @@ func NewKubeControllerCommand(app *kingpin.Application) Command { cmd.Flag("hot-reload-path", "The webhook path for hot-reloading components that allow it.").Default("/-/reload").StringVar(&c.hotReloadPath) cmd.Flag("extra-labels", "Extra labels that will be added to all the generated Prometheus rules ('key=value' form, can be repeated).").Short('l').StringMapVar(&c.extraLabels) cmd.Flag("sli-plugins-path", "The path to SLI plugins (can be repeated), if not set it disable plugins support.").Short('p').StringsVar(&c.sliPluginsPaths) + cmd.Flag("slo-plugins-path", "The path to SLO plugins a.k.a SLO generation middlewares (can be repeated).").Short('m').StringsVar(&c.sloPluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) cmd.Flag("disable-optimized-rules", "If enabled it will disable optimized generated rules.").BoolVar(&c.disableOptimizedRules) @@ -115,7 +117,12 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error sloPeriod := time.Duration(sp) // Plugins. - pluginRepo, err := createPluginLoader(ctx, logger, k.sliPluginsPaths) + pluginSLIRepo, err := createSLIPluginLoader(ctx, logger, k.sliPluginsPaths) + if err != nil { + return err + } + + pluginSLORepo, err := createSLOPluginLoader(ctx, logger, k.sloPluginsPaths) if err != nil { return err } @@ -161,7 +168,11 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error { // Set SLI plugin repository reloader. reloadManager.Add(1000, reload.ReloaderFunc(func(ctx context.Context, id string) error { - return pluginRepo.Reload(ctx) + return pluginSLIRepo.Reload(ctx) + })) + + reloadManager.Add(1000, reload.ReloaderFunc(func(ctx context.Context, id string) error { + return pluginSLORepo.Reload(ctx) })) ctx, cancel := context.WithCancel(ctx) @@ -334,6 +345,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error SLIRulesGenSLOPlugin: sliRuleGen, MetadataRulesGenSLOPlugin: metaRuleGen, AlertRulesGenSLOPlugin: alertRuleGen, + SLOPluginGetter: pluginSLORepo, Logger: generatorLogger{Logger: logger}, }) if err != nil { @@ -343,7 +355,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error // Create handler. controllerConfig := kubecontroller.HandlerConfig{ Generator: generator, - SpecLoader: storageio.NewK8sSlothPrometheusCRSpecLoader(pluginRepo, sloPeriod), + SpecLoader: storageio.NewK8sSlothPrometheusCRSpecLoader(pluginSLIRepo, sloPeriod), Repository: kuberepo, KubeStatusStorer: kuberepo, ExtraLabels: k.extraLabels, diff --git a/cmd/sloth/commands/validate.go b/cmd/sloth/commands/validate.go index 427254e8..f752e627 100644 --- a/cmd/sloth/commands/validate.go +++ b/cmd/sloth/commands/validate.go @@ -23,6 +23,7 @@ type validateCommand struct { slosIncludeRegex string extraLabels map[string]string sliPluginsPaths []string + sloPluginsPaths []string sloPeriodWindowsPath string sloPeriod string } @@ -36,6 +37,7 @@ func NewValidateCommand(app *kingpin.Application) Command { cmd.Flag("fs-include", "Filter regex to include matched discovered SLO file paths, everything else will be ignored. Exclude has preference.").Short('n').StringVar(&c.slosIncludeRegex) cmd.Flag("extra-labels", "Extra labels that will be added to all the generated Prometheus rules ('key=value' form, can be repeated).").Short('l').StringMapVar(&c.extraLabels) cmd.Flag("sli-plugins-path", "The path to SLI plugins (can be repeated), if not set it disable plugins support.").Short('p').StringsVar(&c.sliPluginsPaths) + cmd.Flag("slo-plugins-path", "The path to SLO plugins a.k.a SLO generation middlewares (can be repeated).").Short('m').StringsVar(&c.sloPluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) @@ -81,7 +83,12 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { } // Load plugins. - pluginRepo, err := createPluginLoader(ctx, logger, v.sliPluginsPaths) + pluginSLIRepo, err := createSLIPluginLoader(ctx, logger, v.sliPluginsPaths) + if err != nil { + return err + } + + pluginSLORepo, err := createSLOPluginLoader(ctx, logger, v.sloPluginsPaths) if err != nil { return err } @@ -106,8 +113,8 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { } // Create Spec loaders. - promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) - kubeYAMLLoader := storageio.NewK8sSlothPrometheusYAMLSpecLoader(pluginRepo, sloPeriod) + promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginSLIRepo, sloPeriod) + kubeYAMLLoader := storageio.NewK8sSlothPrometheusYAMLSpecLoader(pluginSLIRepo, sloPeriod) openSLOYAMLLoader := storageio.NewOpenSLOYAMLSpecLoader(sloPeriod) // For every file load the data and start the validation process: @@ -124,9 +131,10 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { splittedSLOsData := splitYAML(slxData) gen := generator{ - logger: log.Noop, - windowsRepo: windowsRepo, - extraLabels: v.extraLabels, + logger: log.Noop, + windowsRepo: windowsRepo, + extraLabels: v.extraLabels, + sloPluginRepo: pluginSLORepo, } // Prepare file validation result and start validation result for every SLO in the file. diff --git a/internal/app/generate/generate.go b/internal/app/generate/generate.go index 093d2efc..6e0a8876 100644 --- a/internal/app/generate/generate.go +++ b/internal/app/generate/generate.go @@ -10,6 +10,8 @@ import ( plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" plugincorenoopsv1 "github.com/slok/sloth/internal/plugin/slo/core/noop_v1" plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" + pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" + commonerrors "github.com/slok/sloth/pkg/common/errors" "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" ) @@ -19,12 +21,25 @@ var ( NoopPlugin, _ = NewSLOProcessorFromSLOPluginV1(plugincorenoopsv1.NewPlugin, log.Noop, nil) ) +type noopSLOPluginGetter bool + +func (noopSLOPluginGetter) GetSLOPlugin(ctx context.Context, id string) (*pluginengineslo.Plugin, error) { + return nil, commonerrors.ErrNotFound +} + +type SLOPluginGetter interface { + GetSLOPlugin(ctx context.Context, id string) (*pluginengineslo.Plugin, error) +} + +//go:generate mockery --case underscore --output generatemock --outpkg generatemock --name SLOPluginGetter + // ServiceConfig is the application service configuration. type ServiceConfig struct { AlertGenerator AlertGenerator SLIRulesGenSLOPlugin SLOProcessor AlertRulesGenSLOPlugin SLOProcessor MetadataRulesGenSLOPlugin SLOProcessor + SLOPluginGetter SLOPluginGetter Logger log.Logger } @@ -33,6 +48,10 @@ func (c *ServiceConfig) defaults() error { return fmt.Errorf("alert generator is required") } + if c.SLOPluginGetter == nil { + c.SLOPluginGetter = noopSLOPluginGetter(false) + } + if c.Logger == nil { c.Logger = log.Noop } @@ -83,9 +102,10 @@ type AlertGenerator interface { // Service is the application service for the generation of SLO for Prometheus. type Service struct { - alertGen AlertGenerator - defaultPlugins []SLOProcessor - logger log.Logger + alertGen AlertGenerator + sloPluginGetter SLOPluginGetter + defaultPlugins []SLOProcessor + logger log.Logger } // NewService returns a new Prometheus application service. @@ -96,7 +116,8 @@ func NewService(config ServiceConfig) (*Service, error) { } return &Service{ - alertGen: config.AlertGenerator, + alertGen: config.AlertGenerator, + sloPluginGetter: config.SLOPluginGetter, defaultPlugins: []SLOProcessor{ config.SLIRulesGenSLOPlugin, config.AlertRulesGenSLOPlugin, @@ -165,8 +186,25 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, slo model.Pro } logger.Debugf("Multiwindow-multiburn alerts generated") - // Generate plugins. + // Generate plugins. Add default plugins and then the ones of each of the SLO. sloProcessors := s.defaultPlugins + for _, p := range slo.Plugins.Plugins { + pf, err := s.sloPluginGetter.GetSLOPlugin(ctx, p.ID) + if err != nil { + return nil, fmt.Errorf("could not get SLO plugin %q: %w", p.ID, err) + } + var processor SLOProcessor + switch { + case pf.PluginV1Factory != nil: + processor, err = NewSLOProcessorFromSLOPluginV1(pf.PluginV1Factory, logger.WithValues(log.Kv{"plugin": pf.ID}), p.Config) + if err != nil { + return nil, fmt.Errorf("could create SLO plugin %q: %w", p.ID, err) + } + } + + sloProcessors = append(sloProcessors, processor) + } + req := &SLOProcessorRequest{ Info: info, MWMBAlertGroup: *as, diff --git a/internal/app/generate/generatemock/slo_plugin_getter.go b/internal/app/generate/generatemock/slo_plugin_getter.go new file mode 100644 index 00000000..e9df51b9 --- /dev/null +++ b/internal/app/generate/generatemock/slo_plugin_getter.go @@ -0,0 +1,60 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package generatemock + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + slo "github.com/slok/sloth/internal/pluginengine/slo" +) + +// SLOPluginGetter is an autogenerated mock type for the SLOPluginGetter type +type SLOPluginGetter struct { + mock.Mock +} + +// GetSLOPlugin provides a mock function with given fields: ctx, id +func (_m *SLOPluginGetter) GetSLOPlugin(ctx context.Context, id string) (*slo.Plugin, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetSLOPlugin") + } + + var r0 *slo.Plugin + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*slo.Plugin, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *slo.Plugin); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*slo.Plugin) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewSLOPluginGetter creates a new instance of SLOPluginGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSLOPluginGetter(t interface { + mock.TestingT + Cleanup(func()) +}) *SLOPluginGetter { + mock := &SLOPluginGetter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go new file mode 100644 index 00000000..614892a4 --- /dev/null +++ b/internal/plugin/plugin.go @@ -0,0 +1,9 @@ +package plugin + +import "embed" + +var ( + //go:embed slo + // Default SLO plugins. These are the default set of SLO plugins that are embedded in the binary. + EmbeddedDefaultSLOPlugins embed.FS +) diff --git a/internal/plugin/slo/core/noop_v1/plugin.go b/internal/plugin/slo/core/noop_v1/plugin.go index 845fd32f..08c08e9f 100644 --- a/internal/plugin/slo/core/noop_v1/plugin.go +++ b/internal/plugin/slo/core/noop_v1/plugin.go @@ -13,11 +13,11 @@ const ( ) func NewPlugin(_ json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { - return noopPlugin{}, nil + return plugin{}, nil } -type noopPlugin struct{} +type plugin struct{} -func (p noopPlugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { return nil } diff --git a/internal/plugin/sli/sli.go b/internal/pluginengine/sli/sli.go similarity index 100% rename from internal/plugin/sli/sli.go rename to internal/pluginengine/sli/sli.go diff --git a/internal/plugin/sli/sli_test.go b/internal/pluginengine/sli/sli_test.go similarity index 98% rename from internal/plugin/sli/sli_test.go rename to internal/pluginengine/sli/sli_test.go index be9e0c45..511b7085 100644 --- a/internal/plugin/sli/sli_test.go +++ b/internal/pluginengine/sli/sli_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/slok/sloth/internal/plugin/sli" + "github.com/slok/sloth/internal/pluginengine/sli" ) func TestSLIPluginLoader(t *testing.T) { diff --git a/internal/plugin/slo/custom/custom.go b/internal/pluginengine/slo/custom/custom.go similarity index 100% rename from internal/plugin/slo/custom/custom.go rename to internal/pluginengine/slo/custom/custom.go diff --git a/internal/plugin/slo/custom/github_com-prometheus-common-model.go b/internal/pluginengine/slo/custom/github_com-prometheus-common-model.go similarity index 100% rename from internal/plugin/slo/custom/github_com-prometheus-common-model.go rename to internal/pluginengine/slo/custom/github_com-prometheus-common-model.go diff --git a/internal/plugin/slo/custom/github_com-prometheus-prometheus-model-rulefmt.go b/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-model-rulefmt.go similarity index 100% rename from internal/plugin/slo/custom/github_com-prometheus-prometheus-model-rulefmt.go rename to internal/pluginengine/slo/custom/github_com-prometheus-prometheus-model-rulefmt.go diff --git a/internal/plugin/slo/custom/github_com-prometheus-prometheus-promql-parser.go b/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go similarity index 100% rename from internal/plugin/slo/custom/github_com-prometheus-prometheus-promql-parser.go rename to internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go diff --git a/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-conventions.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go similarity index 100% rename from internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-conventions.go rename to internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go diff --git a/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-model.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go similarity index 53% rename from internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-model.go rename to internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go index cf4fc0f8..5c73c571 100644 --- a/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-model.go +++ b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go @@ -23,19 +23,21 @@ func init() { "UnknownAlertSeverity": reflect.ValueOf(model.UnknownAlertSeverity), // type definitions - "AlertSeverity": reflect.ValueOf((*model.AlertSeverity)(nil)), - "Info": reflect.ValueOf((*model.Info)(nil)), - "MWMBAlert": reflect.ValueOf((*model.MWMBAlert)(nil)), - "MWMBAlertGroup": reflect.ValueOf((*model.MWMBAlertGroup)(nil)), - "Mode": reflect.ValueOf((*model.Mode)(nil)), - "PromAlertMeta": reflect.ValueOf((*model.PromAlertMeta)(nil)), - "PromRuleGroup": reflect.ValueOf((*model.PromRuleGroup)(nil)), - "PromSLI": reflect.ValueOf((*model.PromSLI)(nil)), - "PromSLIEvents": reflect.ValueOf((*model.PromSLIEvents)(nil)), - "PromSLIRaw": reflect.ValueOf((*model.PromSLIRaw)(nil)), - "PromSLO": reflect.ValueOf((*model.PromSLO)(nil)), - "PromSLOGroup": reflect.ValueOf((*model.PromSLOGroup)(nil)), - "PromSLOGroupSource": reflect.ValueOf((*model.PromSLOGroupSource)(nil)), - "PromSLORules": reflect.ValueOf((*model.PromSLORules)(nil)), + "AlertSeverity": reflect.ValueOf((*model.AlertSeverity)(nil)), + "Info": reflect.ValueOf((*model.Info)(nil)), + "MWMBAlert": reflect.ValueOf((*model.MWMBAlert)(nil)), + "MWMBAlertGroup": reflect.ValueOf((*model.MWMBAlertGroup)(nil)), + "Mode": reflect.ValueOf((*model.Mode)(nil)), + "PromAlertMeta": reflect.ValueOf((*model.PromAlertMeta)(nil)), + "PromRuleGroup": reflect.ValueOf((*model.PromRuleGroup)(nil)), + "PromSLI": reflect.ValueOf((*model.PromSLI)(nil)), + "PromSLIEvents": reflect.ValueOf((*model.PromSLIEvents)(nil)), + "PromSLIRaw": reflect.ValueOf((*model.PromSLIRaw)(nil)), + "PromSLO": reflect.ValueOf((*model.PromSLO)(nil)), + "PromSLOGroup": reflect.ValueOf((*model.PromSLOGroup)(nil)), + "PromSLOGroupSource": reflect.ValueOf((*model.PromSLOGroupSource)(nil)), + "PromSLOPluginMetadata": reflect.ValueOf((*model.PromSLOPluginMetadata)(nil)), + "PromSLORules": reflect.ValueOf((*model.PromSLORules)(nil)), + "SLOPlugins": reflect.ValueOf((*model.SLOPlugins)(nil)), } } diff --git a/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go similarity index 100% rename from internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go rename to internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go diff --git a/internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go similarity index 100% rename from internal/plugin/slo/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go rename to internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go diff --git a/internal/plugin/slo/custom/github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go similarity index 100% rename from internal/plugin/slo/custom/github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go rename to internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-prometheus-plugin-slo-v1.go diff --git a/internal/plugin/slo/slo.go b/internal/pluginengine/slo/slo.go similarity index 95% rename from internal/plugin/slo/slo.go rename to internal/pluginengine/slo/slo.go index 842979d6..a8e76b33 100644 --- a/internal/plugin/slo/slo.go +++ b/internal/pluginengine/slo/slo.go @@ -10,13 +10,13 @@ import ( "github.com/traefik/yaegi/stdlib" "github.com/traefik/yaegi/stdlib/unsafe" - "github.com/slok/sloth/internal/plugin/slo/custom" + "github.com/slok/sloth/internal/pluginengine/slo/custom" pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" ) type Plugin struct { - ID string - PluginFactory pluginslov1.PluginFactory + ID string + PluginV1Factory pluginslov1.PluginFactory } // PluginLoader knows how to load Go SLO plugins using Yaegi. @@ -88,8 +88,8 @@ func (p pluginLoader) LoadRawPlugin(ctx context.Context, src string) (*Plugin, e } return &Plugin{ - ID: pluginID, - PluginFactory: plugin, + ID: pluginID, + PluginV1Factory: plugin, }, nil } diff --git a/internal/plugin/slo/slo_test.go b/internal/pluginengine/slo/slo_test.go similarity index 84% rename from internal/plugin/slo/slo_test.go rename to internal/pluginengine/slo/slo_test.go index 4699741d..49ab092b 100644 --- a/internal/plugin/slo/slo_test.go +++ b/internal/pluginengine/slo/slo_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/slok/sloth/internal/log" - pluginslo "github.com/slok/sloth/internal/plugin/slo" + pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" "github.com/slok/sloth/pkg/common/model" pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" ) @@ -16,18 +16,18 @@ import ( func TestPlugin(t *testing.T) { tests := map[string]struct { pluginSrc string - execPlugin func(t *testing.T, p pluginslo.Plugin) + execPlugin func(t *testing.T, p pluginengineslo.Plugin) expErr bool }{ "Empty plugin should fail.": { pluginSrc: "", - execPlugin: func(t *testing.T, p pluginslo.Plugin) {}, + execPlugin: func(t *testing.T, p pluginengineslo.Plugin) {}, expErr: true, }, "An invalid plugin syntax should fail": { pluginSrc: `package test{`, - execPlugin: func(t *testing.T, p pluginslo.Plugin) {}, + execPlugin: func(t *testing.T, p pluginengineslo.Plugin) {}, expErr: true, }, @@ -55,7 +55,7 @@ func (noopPlugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, return nil } `, - execPlugin: func(t *testing.T, p pluginslo.Plugin) {}, + execPlugin: func(t *testing.T, p pluginengineslo.Plugin) {}, expErr: true, }, @@ -83,7 +83,7 @@ func (noopPlugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, return nil } `, - execPlugin: func(t *testing.T, p pluginslo.Plugin) {}, + execPlugin: func(t *testing.T, p pluginengineslo.Plugin) {}, expErr: true, }, @@ -111,7 +111,7 @@ func (noopPlugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, return nil } `, - execPlugin: func(t *testing.T, p pluginslo.Plugin) {}, + execPlugin: func(t *testing.T, p pluginengineslo.Plugin) {}, expErr: true, }, @@ -146,8 +146,8 @@ func (test) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result return nil } `, - execPlugin: func(t *testing.T, p pluginslo.Plugin) { - plugin, err := p.PluginFactory(nil, pluginslov1.AppUtils{Logger: log.Noop}) + execPlugin: func(t *testing.T, p pluginengineslo.Plugin) { + plugin, err := p.PluginV1Factory(nil, pluginslov1.AppUtils{Logger: log.Noop}) require.NoError(t, err) gotResp := &pluginslov1.Result{} err = plugin.ProcessSLO(t.Context(), &pluginslov1.Request{}, gotResp) @@ -165,7 +165,7 @@ func (test) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result for name, test := range tests { t.Run(name, func(t *testing.T) { assert := assert.New(t) - plugin, err := pluginslo.PluginLoader.LoadRawPlugin(t.Context(), test.pluginSrc) + plugin, err := pluginengineslo.PluginLoader.LoadRawPlugin(t.Context(), test.pluginSrc) if test.expErr { assert.Error(err) } else if assert.NoError(err) { diff --git a/internal/storage/fs/fsmock/sli_plugin_loader.go b/internal/storage/fs/fsmock/sli_plugin_loader.go index 84528ad4..fbadae40 100644 --- a/internal/storage/fs/fsmock/sli_plugin_loader.go +++ b/internal/storage/fs/fsmock/sli_plugin_loader.go @@ -7,7 +7,7 @@ import ( mock "github.com/stretchr/testify/mock" - sli "github.com/slok/sloth/internal/plugin/sli" + sli "github.com/slok/sloth/internal/pluginengine/sli" ) // SLIPluginLoader is an autogenerated mock type for the SLIPluginLoader type diff --git a/internal/storage/fs/fsmock/slo_plugin_loader.go b/internal/storage/fs/fsmock/slo_plugin_loader.go new file mode 100644 index 00000000..d1b4cbf4 --- /dev/null +++ b/internal/storage/fs/fsmock/slo_plugin_loader.go @@ -0,0 +1,60 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package fsmock + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + slo "github.com/slok/sloth/internal/pluginengine/slo" +) + +// SLOPluginLoader is an autogenerated mock type for the SLOPluginLoader type +type SLOPluginLoader struct { + mock.Mock +} + +// LoadRawPlugin provides a mock function with given fields: ctx, src +func (_m *SLOPluginLoader) LoadRawPlugin(ctx context.Context, src string) (*slo.Plugin, error) { + ret := _m.Called(ctx, src) + + if len(ret) == 0 { + panic("no return value specified for LoadRawPlugin") + } + + var r0 *slo.Plugin + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*slo.Plugin, error)); ok { + return rf(ctx, src) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *slo.Plugin); ok { + r0 = rf(ctx, src) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*slo.Plugin) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, src) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewSLOPluginLoader creates a new instance of SLOPluginLoader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSLOPluginLoader(t interface { + mock.TestingT + Cleanup(func()) +}) *SLOPluginLoader { + mock := &SLOPluginLoader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/storage/fs/sli_plugin.go b/internal/storage/fs/sli_plugin.go index c9f6ff10..efa23602 100644 --- a/internal/storage/fs/sli_plugin.go +++ b/internal/storage/fs/sli_plugin.go @@ -10,11 +10,11 @@ import ( "sync" "github.com/slok/sloth/internal/log" - pluginsli "github.com/slok/sloth/internal/plugin/sli" + pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" ) type SLIPluginLoader interface { - LoadRawSLIPlugin(ctx context.Context, src string) (*pluginsli.SLIPlugin, error) + LoadRawSLIPlugin(ctx context.Context, src string) (*pluginenginesli.SLIPlugin, error) } //go:generate mockery --case underscore --output fsmock --outpkg fsmock --name SLIPluginLoader @@ -72,7 +72,7 @@ func (c *FileSLIPluginRepoConfig) defaults() error { } if c.PluginLoader == nil { - c.PluginLoader = pluginsli.PluginLoader + c.PluginLoader = pluginenginesli.PluginLoader } if c.Logger == nil { @@ -122,7 +122,7 @@ type FileSLIPluginRepo struct { pluginLoader SLIPluginLoader fileManager FileManager paths []string - plugins map[string]pluginsli.SLIPlugin + plugins map[string]pluginenginesli.SLIPlugin mu sync.RWMutex logger log.Logger } @@ -144,7 +144,7 @@ func (f *FileSLIPluginRepo) Reload(ctx context.Context) error { } // Load the plugins. - plugins := map[string]pluginsli.SLIPlugin{} + plugins := map[string]pluginenginesli.SLIPlugin{} for path := range paths { pluginData, err := f.fileManager.ReadFile(ctx, path) if err != nil { @@ -177,14 +177,14 @@ func (f *FileSLIPluginRepo) Reload(ctx context.Context) error { return nil } -func (f *FileSLIPluginRepo) ListSLIPlugins(ctx context.Context) (map[string]pluginsli.SLIPlugin, error) { +func (f *FileSLIPluginRepo) ListSLIPlugins(ctx context.Context) (map[string]pluginenginesli.SLIPlugin, error) { f.mu.RLock() defer f.mu.RUnlock() return f.plugins, nil } -func (f *FileSLIPluginRepo) GetSLIPlugin(ctx context.Context, id string) (*pluginsli.SLIPlugin, error) { +func (f *FileSLIPluginRepo) GetSLIPlugin(ctx context.Context, id string) (*pluginenginesli.SLIPlugin, error) { f.mu.RLock() defer f.mu.RUnlock() diff --git a/internal/storage/fs/sli_plugin_test.go b/internal/storage/fs/sli_plugin_test.go index db5f1851..cd3ce8c1 100644 --- a/internal/storage/fs/sli_plugin_test.go +++ b/internal/storage/fs/sli_plugin_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - pluginsli "github.com/slok/sloth/internal/plugin/sli" + pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" "github.com/slok/sloth/internal/storage/fs" "github.com/slok/sloth/internal/storage/fs/fsmock" ) @@ -15,14 +15,14 @@ import ( func TestSLIPluginRepoListSLIPlugins(t *testing.T) { tests := map[string]struct { mock func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) - expPlugins map[string]pluginsli.SLIPlugin + expPlugins map[string]pluginenginesli.SLIPlugin expErr bool }{ "Having no files, should return empty list of plugins.": { mock: func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) { mfm.On("FindFiles", mock.Anything, "./", mock.Anything).Once().Return([]string{}, nil) }, - expPlugins: map[string]pluginsli.SLIPlugin{}, + expPlugins: map[string]pluginenginesli.SLIPlugin{}, }, "Having multiple files, should return multiple plugins.": { @@ -39,12 +39,12 @@ func TestSLIPluginRepoListSLIPlugins(t *testing.T) { mfm.On("ReadFile", mock.Anything, "./test2/test_plugin_3.go").Once().Return([]byte(`test3`), nil) mfm.On("ReadFile", mock.Anything, "./test3/test4/test_plugin_4.go").Once().Return([]byte(`test4`), nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginsli.SLIPlugin{ID: "test1"}, nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginsli.SLIPlugin{ID: "test2"}, nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test3").Once().Return(&pluginsli.SLIPlugin{ID: "test3"}, nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test4").Once().Return(&pluginsli.SLIPlugin{ID: "test4"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginenginesli.SLIPlugin{ID: "test1"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginenginesli.SLIPlugin{ID: "test2"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test3").Once().Return(&pluginenginesli.SLIPlugin{ID: "test3"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test4").Once().Return(&pluginenginesli.SLIPlugin{ID: "test4"}, nil) }, - expPlugins: map[string]pluginsli.SLIPlugin{ + expPlugins: map[string]pluginenginesli.SLIPlugin{ "test1": {ID: "test1"}, "test2": {ID: "test2"}, "test3": {ID: "test3"}, @@ -85,7 +85,7 @@ func TestSLIPluginRepoGetSLIPlugin(t *testing.T) { tests := map[string]struct { pluginID string mock func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) - expPlugin pluginsli.SLIPlugin + expPlugin pluginenginesli.SLIPlugin expErr bool }{ "Having a missing plugin, should fail.": { @@ -99,8 +99,8 @@ func TestSLIPluginRepoGetSLIPlugin(t *testing.T) { mfm.On("ReadFile", mock.Anything, "./test_plugin_1.go").Once().Return([]byte(`test1`), nil) mfm.On("ReadFile", mock.Anything, "./test_plugin_2.go").Once().Return([]byte(`test2`), nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginsli.SLIPlugin{ID: "test1"}, nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginsli.SLIPlugin{ID: "test2"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginenginesli.SLIPlugin{ID: "test1"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginenginesli.SLIPlugin{ID: "test2"}, nil) }, expErr: true, }, @@ -116,10 +116,10 @@ func TestSLIPluginRepoGetSLIPlugin(t *testing.T) { mfm.On("ReadFile", mock.Anything, "./test_plugin_1.go").Once().Return([]byte(`test1`), nil) mfm.On("ReadFile", mock.Anything, "./test_plugin_2.go").Once().Return([]byte(`test2`), nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginsli.SLIPlugin{ID: "test1"}, nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginsli.SLIPlugin{ID: "test2"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginenginesli.SLIPlugin{ID: "test1"}, nil) + mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginenginesli.SLIPlugin{ID: "test2"}, nil) }, - expPlugin: pluginsli.SLIPlugin{ID: "test2"}, + expPlugin: pluginenginesli.SLIPlugin{ID: "test2"}, }, } diff --git a/internal/storage/fs/slo_plugin.go b/internal/storage/fs/slo_plugin.go new file mode 100644 index 00000000..b1f17395 --- /dev/null +++ b/internal/storage/fs/slo_plugin.go @@ -0,0 +1,124 @@ +package fs + +import ( + "context" + "fmt" + "io/fs" + "regexp" + "sync" + + "github.com/slok/sloth/internal/log" + pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" + commonerrors "github.com/slok/sloth/pkg/common/errors" +) + +type SLOPluginLoader interface { + LoadRawPlugin(ctx context.Context, src string) (*pluginengineslo.Plugin, error) +} + +//go:generate mockery --case underscore --output fsmock --outpkg fsmock --name SLOPluginLoader + +type FileSLOPluginRepo struct { + fss []fs.FS + pluginLoader SLOPluginLoader + cache map[string]pluginengineslo.Plugin + logger log.Logger + mu sync.RWMutex +} + +// NewFileSLOPluginRepo returns a new FileSLOPluginRepo that loads plugins from the given file system. +// The plugin file should be called "plugin.go". +func NewFileSLOPluginRepo(logger log.Logger, pluginLoader SLOPluginLoader, fss ...fs.FS) (*FileSLOPluginRepo, error) { + r := &FileSLOPluginRepo{ + fss: fss, + pluginLoader: pluginLoader, + cache: map[string]pluginengineslo.Plugin{}, + logger: logger, + } + + err := r.Reload(context.Background()) + if err != nil { + return nil, fmt.Errorf("could not load plugins: %w", err) + } + + return r, nil +} + +var sloPluginNameRegex = regexp.MustCompile("plugin.go$") + +func (r *FileSLOPluginRepo) Reload(ctx context.Context) error { + plugins, err := r.loadPlugins(ctx, r.fss...) + if err != nil { + return fmt.Errorf("could not load plugins: %w", err) + } + + // Set loaded plugins. + r.mu.Lock() + r.cache = plugins + r.mu.Unlock() + + r.logger.WithValues(log.Kv{"plugins": len(plugins)}).Infof("SLO plugins loaded") + return nil +} + +func (r *FileSLOPluginRepo) GetSLOPlugin(ctx context.Context, id string) (*pluginengineslo.Plugin, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + p, ok := r.cache[id] + if !ok { + return nil, fmt.Errorf("plugin %q not found: %w", id, commonerrors.ErrNotFound) + } + + return &p, nil +} + +func (r *FileSLOPluginRepo) ListSLOPlugins(ctx context.Context) (map[string]pluginengineslo.Plugin, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + return r.cache, nil +} + +func (r *FileSLOPluginRepo) loadPlugins(ctx context.Context, fss ...fs.FS) (map[string]pluginengineslo.Plugin, error) { + allPlugins := map[string]pluginengineslo.Plugin{} + + for _, f := range fss { + err := fs.WalkDir(f, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + + if !sloPluginNameRegex.MatchString(path) { + return nil + } + + pluginData, err := fs.ReadFile(f, path) + if err != nil { + return fmt.Errorf("could not read %q plugin data: %w", path, err) + } + + plugin, err := r.pluginLoader.LoadRawPlugin(ctx, string(pluginData)) + if err != nil { + return fmt.Errorf("could not load %q plugin: %w", path, err) + } + + _, ok := allPlugins[plugin.ID] + if ok { + return fmt.Errorf("plugin %q already loaded", plugin.ID) + } + allPlugins[plugin.ID] = *plugin + r.logger.WithValues(log.Kv{"plugin-id": plugin.ID}).Debugf("SLO plugin discovered and loaded") + + return nil + }) + if err != nil { + return nil, fmt.Errorf("could not walk dir: %w", err) + } + } + + return allPlugins, nil +} diff --git a/internal/storage/fs/slo_plugin_test.go b/internal/storage/fs/slo_plugin_test.go new file mode 100644 index 00000000..8a36bfa2 --- /dev/null +++ b/internal/storage/fs/slo_plugin_test.go @@ -0,0 +1,178 @@ +package fs_test + +import ( + "fmt" + "io/fs" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/slok/sloth/internal/log" + pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" + storagefs "github.com/slok/sloth/internal/storage/fs" + "github.com/slok/sloth/internal/storage/fs/fsmock" +) + +func TestFileSLOPluginRepoListSLOPlugins(t *testing.T) { + tests := map[string]struct { + fss func() []fs.FS + mock func(mpl *fsmock.SLOPluginLoader) + expPlugins map[string]pluginengineslo.Plugin + expLoadErr bool + expErr bool + }{ + "Having no files, should return empty list of plugins.": { + fss: func() []fs.FS { return nil }, + mock: func(mpl *fsmock.SLOPluginLoader) {}, + expPlugins: map[string]pluginengineslo.Plugin{}, + }, + + "Having plugins in multiple FS and directories, should return all plugins.": { + fss: func() []fs.FS { + m1 := make(fstest.MapFS) + m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + m1["m1/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p3")} + + m2 := make(fstest.MapFS) + m2["m2/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p4")} + m2["m2/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p5")} + m2["m2/plugin-test.go"] = &fstest.MapFile{Data: []byte("p8")} // Ignored. + + m3 := make(fstest.MapFS) + m3["m3/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p6")} + m3["m3/plx/pl3/plugin.yaml"] = &fstest.MapFile{Data: []byte("p7")} // Ignored. + + return []fs.FS{m1, m2, m3} + }, + mock: func(mpl *fsmock.SLOPluginLoader) { + mpl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) + mpl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(&pluginengineslo.Plugin{ID: "p2"}, nil) + mpl.On("LoadRawPlugin", mock.Anything, "p3").Once().Return(&pluginengineslo.Plugin{ID: "p3"}, nil) + mpl.On("LoadRawPlugin", mock.Anything, "p4").Once().Return(&pluginengineslo.Plugin{ID: "p4"}, nil) + mpl.On("LoadRawPlugin", mock.Anything, "p5").Once().Return(&pluginengineslo.Plugin{ID: "p5"}, nil) + mpl.On("LoadRawPlugin", mock.Anything, "p6").Once().Return(&pluginengineslo.Plugin{ID: "p6"}, nil) + }, + expPlugins: map[string]pluginengineslo.Plugin{ + "p1": {ID: "p1"}, + "p2": {ID: "p2"}, + "p3": {ID: "p3"}, + "p4": {ID: "p4"}, + "p5": {ID: "p5"}, + "p6": {ID: "p6"}, + }, + }, + + "Having a plugin loaded with the same ID multiple times should fail.": { + fss: func() []fs.FS { + m1 := make(fstest.MapFS) + m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + + return []fs.FS{m1} + }, + mock: func(mpl *fsmock.SLOPluginLoader) { + mpl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) + mpl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) + + }, + expLoadErr: true, + }, + + "Having an error while loading a plugin, should fail.": { + fss: func() []fs.FS { + m1 := make(fstest.MapFS) + m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + + return []fs.FS{m1} + }, + mock: func(mpl *fsmock.SLOPluginLoader) { + mpl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) + mpl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + + }, + expLoadErr: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + mpl := fsmock.NewSLOPluginLoader(t) + test.mock(mpl) + + // Create repository and load plugins. + repo, err := storagefs.NewFileSLOPluginRepo(log.Noop, mpl, test.fss()...) + if test.expLoadErr { + assert.Error(err) + return + } + assert.NoError(err) + + plugins, err := repo.ListSLOPlugins(t.Context()) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expPlugins, plugins) + } + }) + } +} + +func TestFileSLOPluginRepoGetSLOPlugin(t *testing.T) { + tests := map[string]struct { + pluginID string + fss func() []fs.FS + mock func(mpl *fsmock.SLOPluginLoader) + expPlugin pluginengineslo.Plugin + expErr bool + }{ + "Having no files, should return empty list of plugins.": { + pluginID: "test", + fss: func() []fs.FS { return nil }, + mock: func(mpl *fsmock.SLOPluginLoader) {}, + expErr: true, + }, + + "Getting a correct plugin, should return the plugin.": { + pluginID: "p2", + fss: func() []fs.FS { + m := make(fstest.MapFS) + m["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + return []fs.FS{m} + }, + mock: func(mpl *fsmock.SLOPluginLoader) { + mpl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) + mpl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(&pluginengineslo.Plugin{ID: "p2"}, nil) + }, + expPlugin: pluginengineslo.Plugin{ID: "p2"}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + mpl := fsmock.NewSLOPluginLoader(t) + test.mock(mpl) + + // Create repository and load plugins. + repo, err := storagefs.NewFileSLOPluginRepo(log.Noop, mpl, test.fss()...) + require.NoError(err) + + plugin, err := repo.GetSLOPlugin(t.Context(), test.pluginID) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expPlugin, *plugin) + } + }) + } +} diff --git a/internal/storage/io/k8s_sloth_test.go b/internal/storage/io/k8s_sloth_test.go index c1230446..d6864108 100644 --- a/internal/storage/io/k8s_sloth_test.go +++ b/internal/storage/io/k8s_sloth_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - pluginsli "github.com/slok/sloth/internal/plugin/sli" + pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" kubeslothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" @@ -18,7 +18,7 @@ import ( func TestK8sSlothPrometheusYAMLSpecLoader(t *testing.T) { tests := map[string]struct { specYaml string - plugins map[string]pluginsli.SLIPlugin + plugins map[string]pluginenginesli.SLIPlugin expModel *model.PromSLOGroup expErr bool }{ @@ -107,7 +107,7 @@ spec: }, "Spec with SLI plugin should use the plugin correctly.": { - plugins: map[string]pluginsli.SLIPlugin{ + plugins: map[string]pluginenginesli.SLIPlugin{ "test_plugin": { ID: "test_plugin", Func: func(ctx context.Context, meta map[string]string, labels map[string]string, options map[string]string) (string, error) { @@ -190,7 +190,7 @@ spec: }, "An spec with SLI plugin that returns an error should use the plugin correctly and fail.": { - plugins: map[string]pluginsli.SLIPlugin{ + plugins: map[string]pluginenginesli.SLIPlugin{ "test_plugin": { ID: "test_plugin", Func: func(ctx context.Context, meta map[string]string, labels map[string]string, options map[string]string) (string, error) { @@ -472,7 +472,7 @@ kind: PrometheusServiceLevel t.Run(name, func(t *testing.T) { assert := assert.New(t) - loader := io.NewK8sSlothPrometheusYAMLSpecLoader(testMemPluginsRepo(map[string]pluginsli.SLIPlugin{}), 30*24*time.Hour) + loader := io.NewK8sSlothPrometheusYAMLSpecLoader(testMemPluginsRepo(map[string]pluginenginesli.SLIPlugin{}), 30*24*time.Hour) got := loader.IsSpecType(context.TODO(), []byte(test.specYaml)) assert.Equal(test.exp, got) diff --git a/internal/storage/io/sloth.go b/internal/storage/io/sloth.go index e4220cef..2e4041e7 100644 --- a/internal/storage/io/sloth.go +++ b/internal/storage/io/sloth.go @@ -8,7 +8,7 @@ import ( "gopkg.in/yaml.v2" - pluginsli "github.com/slok/sloth/internal/plugin/sli" + pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" prometheusv1 "github.com/slok/sloth/pkg/prometheus/api/v1" @@ -16,7 +16,7 @@ import ( ) type SLIPluginRepo interface { - GetSLIPlugin(ctx context.Context, id string) (*pluginsli.SLIPlugin, error) + GetSLIPlugin(ctx context.Context, id string) (*pluginenginesli.SLIPlugin, error) } // SlothPrometheusYAMLSpecLoader knows how to load sloth prometheus YAML specs and converts them to a model. diff --git a/internal/storage/io/sloth_test.go b/internal/storage/io/sloth_test.go index 5d6b5294..a3ceeebb 100644 --- a/internal/storage/io/sloth_test.go +++ b/internal/storage/io/sloth_test.go @@ -8,15 +8,15 @@ import ( "github.com/stretchr/testify/assert" - pluginsli "github.com/slok/sloth/internal/plugin/sli" + pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" v1 "github.com/slok/sloth/pkg/prometheus/api/v1" ) -type testMemPluginsRepo map[string]pluginsli.SLIPlugin +type testMemPluginsRepo map[string]pluginenginesli.SLIPlugin -func (t testMemPluginsRepo) GetSLIPlugin(ctx context.Context, id string) (*pluginsli.SLIPlugin, error) { +func (t testMemPluginsRepo) GetSLIPlugin(ctx context.Context, id string) (*pluginenginesli.SLIPlugin, error) { p, ok := t[id] if !ok { return nil, fmt.Errorf("unknown plugin") @@ -27,7 +27,7 @@ func (t testMemPluginsRepo) GetSLIPlugin(ctx context.Context, id string) (*plugi func TestSlothPrometheusYAMLSpecLoader(t *testing.T) { tests := map[string]struct { specYaml string - plugins map[string]pluginsli.SLIPlugin + plugins map[string]pluginenginesli.SLIPlugin windowPeriod time.Duration expModel *model.PromSLOGroup expErr bool @@ -90,7 +90,7 @@ slos: }, "Spec with SLI plugin that returns an error should use the plugin correctly and fail.": { - plugins: map[string]pluginsli.SLIPlugin{ + plugins: map[string]pluginenginesli.SLIPlugin{ "test_plugin": { ID: "test_plugin", Func: func(ctx context.Context, meta map[string]string, labels map[string]string, options map[string]string) (string, error) { @@ -118,7 +118,7 @@ slos: "Spec with SLI plugin should use the plugin correctly.": { windowPeriod: 30 * 24 * time.Hour, - plugins: map[string]pluginsli.SLIPlugin{ + plugins: map[string]pluginenginesli.SLIPlugin{ "test_plugin": { ID: "test_plugin", Func: func(ctx context.Context, meta map[string]string, labels map[string]string, options map[string]string) (string, error) { @@ -468,7 +468,7 @@ func TestSlothPrometheusYAMLSpecLoaderIsSpecType(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - loader := io.NewSlothPrometheusYAMLSpecLoader(testMemPluginsRepo(map[string]pluginsli.SLIPlugin{}), 0) + loader := io.NewSlothPrometheusYAMLSpecLoader(testMemPluginsRepo(map[string]pluginenginesli.SLIPlugin{}), 0) got := loader.IsSpecType(context.TODO(), []byte(test.specYaml)) assert.Equal(test.exp, got) diff --git a/pkg/common/errors/errors.go b/pkg/common/errors/errors.go index 161218ec..6f66e27d 100644 --- a/pkg/common/errors/errors.go +++ b/pkg/common/errors/errors.go @@ -6,4 +6,7 @@ var ( // ErrNoSLORules will be used when there are no rules to store. The upper layer // could ignore or handle the error in cases where there wasn't an output. ErrNoSLORules = fmt.Errorf("0 SLO Prometheus rules generated") + + // ErrNotFound will be used when a resource has not been found. + ErrNotFound = fmt.Errorf("resource not found") ) diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index 43d1ef43..db3bffc9 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -53,6 +53,16 @@ type PromSLO struct { Labels map[string]string `validate:"dive,keys,prom_label_key,endkeys,required,prom_label_value"` PageAlertMeta PromAlertMeta TicketAlertMeta PromAlertMeta + Plugins SLOPlugins +} + +type SLOPlugins struct { + Plugins []PromSLOPluginMetadata +} + +type PromSLOPluginMetadata struct { + ID string + Config any } type PromSLOGroup struct { diff --git a/pkg/prometheus/plugin/slo/v1/testing/testing.go b/pkg/prometheus/plugin/slo/v1/testing/testing.go index 486a34d0..478d6f69 100644 --- a/pkg/prometheus/plugin/slo/v1/testing/testing.go +++ b/pkg/prometheus/plugin/slo/v1/testing/testing.go @@ -7,7 +7,7 @@ import ( "os" "github.com/slok/sloth/internal/log" - pluginslo "github.com/slok/sloth/internal/plugin/slo" + pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" ) @@ -45,12 +45,12 @@ func NewTestPlugin(ctx context.Context, config TestPluginConfig) (pluginslov1.Pl if err != nil { return nil, fmt.Errorf("could not read plugin source code: %w", err) } - plugin, err := pluginslo.PluginLoader.LoadRawPlugin(ctx, string(pluginSource)) + plugin, err := pluginengineslo.PluginLoader.LoadRawPlugin(ctx, string(pluginSource)) if err != nil { return nil, fmt.Errorf("could not load plugin source code: %w", err) } - return plugin.PluginFactory(config.PluginConfiguration, pluginslov1.AppUtils{ + return plugin.PluginV1Factory(config.PluginConfiguration, pluginslov1.AppUtils{ Logger: log.Noop, }) } From 7aa2eca4bd26b085791890c812b50874fcd2fa81 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 5 Apr 2025 08:50:17 +0200 Subject: [PATCH 038/173] Add missing SLO plugin unit test usage on domain app Signed-off-by: Xabier Larrakoetxea --- internal/app/generate/generate_test.go | 104 ++++++++++++++++++------- 1 file changed, 76 insertions(+), 28 deletions(-) diff --git a/internal/app/generate/generate_test.go b/internal/app/generate/generate_test.go index 2b57ad64..d43d6539 100644 --- a/internal/app/generate/generate_test.go +++ b/internal/app/generate/generate_test.go @@ -2,30 +2,61 @@ package generate_test import ( "context" + "encoding/json" "testing" "time" "github.com/prometheus/prometheus/model/rulefmt" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/app/generate" + "github.com/slok/sloth/internal/app/generate/generatemock" + pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" ) +type testPluginAlertInterval struct { + interval time.Duration +} + +func (p testPluginAlertInterval) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + result.SLORules.AlertRules.Interval = p.interval + return nil +} + func TestIntegrationAppServiceGenerate(t *testing.T) { tests := map[string]struct { + mocks func(mspg *generatemock.SLOPluginGetter) req generate.Request expResp generate.Response expErr bool }{ "If no SLOs are requested it should error.": { + mocks: func(mspg *generatemock.SLOPluginGetter) {}, req: generate.Request{}, expErr: true, }, "Having SLOs it should generate Prometheus recording and alert rules.": { + mocks: func(mspg *generatemock.SLOPluginGetter) { + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin1").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin1", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertInterval{interval: 42 * time.Minute}, nil + }, + }, nil) + + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin2").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin2", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertInterval{interval: 99 * time.Minute}, nil + }, + }, nil) + }, req: generate.Request{ ExtraLabels: map[string]string{ "extra_k1": "extra_v1", @@ -60,6 +91,12 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { Labels: map[string]string{"t_alert_label": "t_label_al_1"}, Annotations: map[string]string{"t_alert_annot": "t_label_an_1"}, }, + Plugins: model.SLOPlugins{ + Plugins: []model.PromSLOPluginMetadata{ + {ID: "test-plugin1", Config: map[string]any{"arg1": "val1"}}, + {ID: "test-plugin2", Config: map[string]any{"arg2": "val2"}}, + }, + }, }, }}, }, @@ -93,6 +130,12 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { Labels: map[string]string{"t_alert_label": "t_label_al_1"}, Annotations: map[string]string{"t_alert_annot": "t_label_an_1"}, }, + Plugins: model.SLOPlugins{ + Plugins: []model.PromSLOPluginMetadata{ + {ID: "test-plugin1", Config: map[string]any{"arg1": "val1"}}, + {ID: "test-plugin2", Config: map[string]any{"arg2": "val2"}}, + }, + }, }, SLORules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ @@ -298,11 +341,12 @@ slo:error_budget:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo=" }, }, }}, - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - - { - Alert: "p_alert_test_name", - Expr: `( + AlertRules: model.PromRuleGroup{ + Interval: 99 * time.Minute, // From the SLO plugins. + Rules: []rulefmt.Rule{ + { + Alert: "p_alert_test_name", + Expr: `( max(slo:sli_error:ratio_rate5m{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} > (14.4 * 0.0009999999999999432)) without (sloth_window) and max(slo:sli_error:ratio_rate1h{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} > (14.4 * 0.0009999999999999432)) without (sloth_window) @@ -314,19 +358,19 @@ or max(slo:sli_error:ratio_rate6h{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} > (6 * 0.0009999999999999432)) without (sloth_window) ) `, - Labels: map[string]string{ - "p_alert_label": "p_label_al_1", - "sloth_severity": "page", + Labels: map[string]string{ + "p_alert_label": "p_label_al_1", + "sloth_severity": "page", + }, + Annotations: map[string]string{ + "p_alert_annot": "p_label_an_1", + "summary": "{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is over expected.", + "title": "(page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast.", + }, }, - Annotations: map[string]string{ - "p_alert_annot": "p_label_an_1", - "summary": "{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is over expected.", - "title": "(page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast.", - }, - }, - { - Alert: "t_alert_test_name", - Expr: `( + { + Alert: "t_alert_test_name", + Expr: `( max(slo:sli_error:ratio_rate2h{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} > (3 * 0.0009999999999999432)) without (sloth_window) and max(slo:sli_error:ratio_rate1d{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} > (3 * 0.0009999999999999432)) without (sloth_window) @@ -338,17 +382,17 @@ or max(slo:sli_error:ratio_rate3d{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} > (1 * 0.0009999999999999432)) without (sloth_window) ) `, - Labels: map[string]string{ - "t_alert_label": "t_label_al_1", - "sloth_severity": "ticket", - }, - Annotations: map[string]string{ - "t_alert_annot": "t_label_an_1", - "summary": "{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is over expected.", - "title": "(ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast.", + Labels: map[string]string{ + "t_alert_label": "t_label_al_1", + "sloth_severity": "ticket", + }, + Annotations: map[string]string{ + "t_alert_annot": "t_label_an_1", + "summary": "{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is over expected.", + "title": "(ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast.", + }, }, - }, - }}, + }}, }, }, }, @@ -361,11 +405,15 @@ or assert := assert.New(t) require := require.New(t) + mspg := generatemock.NewSLOPluginGetter(t) + test.mocks(mspg) + windowsRepo, err := alert.NewFSWindowsRepo(alert.FSWindowsRepoConfig{}) require.NoError(err) svc, err := generate.NewService(generate.ServiceConfig{ - AlertGenerator: alert.NewGenerator(windowsRepo), + AlertGenerator: alert.NewGenerator(windowsRepo), + SLOPluginGetter: mspg, }) require.NoError(err) From c7a1e6aaff87ccca0c12d95bb2145150f9415ea2 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sun, 6 Apr 2025 10:12:07 +0200 Subject: [PATCH 039/173] Add priority to have SLO plugins execution order in the chain Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 1 + internal/app/generate/generate.go | 26 +- internal/app/generate/generate_test.go | 381 +++++++++++++++++++++++++ pkg/common/model/slo_prometheus.go | 5 +- 4 files changed, 407 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d15ad7b..e8981ab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Sloth conventions can be imported in Go apps using `github.com/slok/sloth/pkg/common/conventions`. - A new SLO rule generation plugin system has been added to be able to change/extend the SLO rule generation process. - SLO plugins can be loaded from FS directories recursively using `--slo-plugins-path` in the commands. +- SLO plugins have a priority value to be able to order in the execution chain. ## [v0.12.0] - 2025-03-27 diff --git a/internal/app/generate/generate.go b/internal/app/generate/generate.go index 6e0a8876..bbde914d 100644 --- a/internal/app/generate/generate.go +++ b/internal/app/generate/generate.go @@ -1,8 +1,10 @@ package generate import ( + "cmp" "context" "fmt" + "slices" "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/log" @@ -186,9 +188,17 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, slo model.Pro } logger.Debugf("Multiwindow-multiburn alerts generated") - // Generate plugins. Add default plugins and then the ones of each of the SLO. - sloProcessors := s.defaultPlugins - for _, p := range slo.Plugins.Plugins { + // Get SLO plugins based on the priority, default plugins are `0` priority + // so, we split the plugins in two slices, pre default (<0) and post default (>=0). + // That way we create the final processor list: pre-default + default + post-default. + preDefault := []SLOProcessor{} + postDefault := []SLOProcessor{} + sloPluginMetadata := append([]model.PromSLOPluginMetadata{}, slo.Plugins.Plugins...) + slices.SortStableFunc(sloPluginMetadata, func(a, b model.PromSLOPluginMetadata) int { + return cmp.Compare(a.Priority, b.Priority) + }) + + for _, p := range sloPluginMetadata { pf, err := s.sloPluginGetter.GetSLOPlugin(ctx, p.ID) if err != nil { return nil, fmt.Errorf("could not get SLO plugin %q: %w", p.ID, err) @@ -202,9 +212,17 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, slo model.Pro } } - sloProcessors = append(sloProcessors, processor) + if p.Priority < 0 { + preDefault = append(preDefault, processor) + } else { + postDefault = append(postDefault, processor) + } } + // Prepare processors. + sloProcessors := append(preDefault, s.defaultPlugins...) + sloProcessors = append(sloProcessors, postDefault...) + req := &SLOProcessorRequest{ Info: info, MWMBAlertGroup: *as, diff --git a/internal/app/generate/generate_test.go b/internal/app/generate/generate_test.go index d43d6539..d7ab45ba 100644 --- a/internal/app/generate/generate_test.go +++ b/internal/app/generate/generate_test.go @@ -28,6 +28,17 @@ func (p testPluginAlertInterval) ProcessSLO(ctx context.Context, request *plugin return nil } +// testPluginAlertRuleAppender is a test plugin that appends a rule to the +// SLO rules. It is used to test the plugin priority. +type testPluginAlertRuleAppender struct { + rule rulefmt.Rule +} + +func (p testPluginAlertRuleAppender) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + result.SLORules.AlertRules.Rules = append(result.SLORules.AlertRules.Rules, p.rule) + return nil +} + func TestIntegrationAppServiceGenerate(t *testing.T) { tests := map[string]struct { mocks func(mspg *generatemock.SLOPluginGetter) @@ -398,6 +409,376 @@ or }, }, }, + + "Having multiple SLO plugins should execute the plugins in order and generate the rules correctly.": { + mocks: func(mspg *generatemock.SLOPluginGetter) { + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin1").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin1", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertRuleAppender{rule: rulefmt.Rule{Expr: "test1"}}, nil + }, + }, nil) + + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin2").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin2", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertRuleAppender{rule: rulefmt.Rule{Expr: "test2"}}, nil + }, + }, nil) + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin3").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin2", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertRuleAppender{rule: rulefmt.Rule{Expr: "test3"}}, nil + }, + }, nil) + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin4").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin2", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertRuleAppender{rule: rulefmt.Rule{Expr: "test4"}}, nil + }, + }, nil) + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin5").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin2", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertRuleAppender{rule: rulefmt.Rule{Expr: "test5"}}, nil + }, + }, nil) + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin6").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin2", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertRuleAppender{rule: rulefmt.Rule{Expr: "test6"}}, nil + }, + }, nil) + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin7").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin2", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertRuleAppender{rule: rulefmt.Rule{Expr: "test7"}}, nil + }, + }, nil) + }, + req: generate.Request{ + ExtraLabels: map[string]string{ + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + }, + Info: model.Info{ + Version: "test-ver", + Mode: model.ModeTest, + Spec: "test-spec", + }, + SLOGroup: model.PromSLOGroup{SLOs: []model.PromSLO{ + { + ID: "test-id", + Name: "test-name", + Service: "test-svc", + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `rate(my_metric{error="true"}[{{.window}}])`, + TotalQuery: `rate(my_metric[{{.window}}])`, + }, + }, + TimeWindow: 30 * 24 * time.Hour, + Objective: 99.9, + Labels: map[string]string{"test_label": "label_1"}, + PageAlertMeta: model.PromAlertMeta{ + Name: "p_alert_test_name", + Labels: map[string]string{"p_alert_label": "p_label_al_1"}, + Annotations: map[string]string{"p_alert_annot": "p_label_an_1"}, + }, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, + Plugins: model.SLOPlugins{ + Plugins: []model.PromSLOPluginMetadata{ + {ID: "test-plugin1", Priority: 10}, + {ID: "test-plugin2", Priority: -99999}, + {ID: "test-plugin3", Priority: -1}, + {ID: "test-plugin4", Priority: 9999}, + {ID: "test-plugin5", Priority: -20}, + {ID: "test-plugin6", Priority: 0}, + {ID: "test-plugin7", Priority: 1}, + }, + }, + }, + }}, + }, + expResp: generate.Response{ + PrometheusSLOs: []generate.SLOResult{ + { + SLO: model.PromSLO{ + ID: "test-id", + Name: "test-name", + Service: "test-svc", + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `rate(my_metric{error="true"}[{{.window}}])`, + TotalQuery: `rate(my_metric[{{.window}}])`, + }, + }, + TimeWindow: 30 * 24 * time.Hour, + Objective: 99.9, + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + }, + PageAlertMeta: model.PromAlertMeta{ + Name: "p_alert_test_name", + Labels: map[string]string{"p_alert_label": "p_label_al_1"}, + Annotations: map[string]string{"p_alert_annot": "p_label_an_1"}, + }, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, + Plugins: model.SLOPlugins{ + Plugins: []model.PromSLOPluginMetadata{ + {ID: "test-plugin1", Priority: 10}, + {ID: "test-plugin2", Priority: -99999}, + {ID: "test-plugin3", Priority: -1}, + {ID: "test-plugin4", Priority: 9999}, + {ID: "test-plugin5", Priority: -20}, + {ID: "test-plugin6", Priority: 0}, + {ID: "test-plugin7", Priority: 1}, + }, + }, + }, + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Expr: "(rate(my_metric{error=\"true\"}[5m]))\n/\n(rate(my_metric[5m]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "5m", + }, + }, + { + Record: "slo:sli_error:ratio_rate30m", + Expr: "(rate(my_metric{error=\"true\"}[30m]))\n/\n(rate(my_metric[30m]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "30m", + }, + }, + { + Record: "slo:sli_error:ratio_rate1h", + Expr: "(rate(my_metric{error=\"true\"}[1h]))\n/\n(rate(my_metric[1h]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "1h", + }, + }, + { + Record: "slo:sli_error:ratio_rate2h", + Expr: "(rate(my_metric{error=\"true\"}[2h]))\n/\n(rate(my_metric[2h]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "2h", + }, + }, + { + Record: "slo:sli_error:ratio_rate6h", + Expr: "(rate(my_metric{error=\"true\"}[6h]))\n/\n(rate(my_metric[6h]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "6h", + }, + }, + { + Record: "slo:sli_error:ratio_rate1d", + Expr: "(rate(my_metric{error=\"true\"}[1d]))\n/\n(rate(my_metric[1d]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "1d", + }, + }, + { + Record: "slo:sli_error:ratio_rate3d", + Expr: "(rate(my_metric{error=\"true\"}[3d]))\n/\n(rate(my_metric[3d]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "3d", + }, + }, + { + Record: "slo:sli_error:ratio_rate30d", + Expr: "sum_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"test-id\", sloth_service=\"test-svc\", sloth_slo=\"test-name\"}[30d])\n/ ignoring (sloth_window)\ncount_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"test-id\", sloth_service=\"test-svc\", sloth_slo=\"test-name\"}[30d])\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "30d", + }, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + // Metadata labels. + { + Record: "slo:objective:ratio", + Expr: "vector(0.9990000000000001)", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, + }, + { + Record: "slo:error_budget:ratio", + Expr: "vector(1-0.9990000000000001)", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, + }, + { + Record: "slo:time_period:days", + Expr: "vector(30)", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, + }, + { + Record: "slo:current_burn_rate:ratio", + Expr: `slo:sli_error:ratio_rate5m{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} +/ on(sloth_id, sloth_slo, sloth_service) group_left +slo:error_budget:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} +`, + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, + }, + { + Record: "slo:period_burn_rate:ratio", + Expr: `slo:sli_error:ratio_rate30d{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} +/ on(sloth_id, sloth_slo, sloth_service) group_left +slo:error_budget:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} +`, + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, + }, + { + Record: "slo:period_error_budget_remaining:ratio", + Expr: `1 - slo:period_burn_rate:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"}`, + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, + }, + { + Record: "sloth_slo_info", + Expr: `vector(1)`, + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_mode": "test", + "sloth_version": "test-ver", + "sloth_spec": "test-spec", + "sloth_objective": "99.9", + }, + }, + }}, + AlertRules: model.PromRuleGroup{ + Rules: []rulefmt.Rule{ + { + Alert: "p_alert_test_name", + Expr: `( + max(slo:sli_error:ratio_rate5m{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} > (14.4 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1h{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} > (14.4 * 0.0009999999999999432)) without (sloth_window) +) +or +( + max(slo:sli_error:ratio_rate30m{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} > (6 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate6h{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} > (6 * 0.0009999999999999432)) without (sloth_window) +) +`, + Labels: map[string]string{ + "p_alert_label": "p_label_al_1", + "sloth_severity": "page", + }, + Annotations: map[string]string{ + "p_alert_annot": "p_label_an_1", + "summary": "{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is over expected.", + "title": "(page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast.", + }, + }, + // Expected plugins appended and ordered based on priority. + {Expr: "test6"}, + {Expr: "test7"}, + {Expr: "test1"}, + {Expr: "test4"}, + }}, + }, + }, + }, + }, + }, } for name, test := range tests { diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index db3bffc9..416de9cd 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -61,8 +61,9 @@ type SLOPlugins struct { } type PromSLOPluginMetadata struct { - ID string - Config any + ID string + Config any + Priority int } type PromSLOGroup struct { From 4d6df1d4520b518a69571a345cc8b0f1a3daae16 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Gallego Date: Sun, 6 Apr 2025 13:24:28 +0200 Subject: [PATCH 040/173] Update project status on readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7609429d..688ab620 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ ## Project status -Sloth is [still maintained](https://github.com/slok/sloth/issues/521#issuecomment-2745271807). +Sloth is actively maintained and in continuous development. -After a period of inactivity, the project is now being updated. It will take some time to catch up, but we’ll get there. More updates coming in the next few weeks. +We’re currently working on internal improvements to make Sloth even more extensible and adaptable to real-world use cases. While Sloth already supports custom SLI plugins, upcoming changes will bring more flexibility to how SLOs are generated, making it easier to adapt Sloth to your custom needs. Stay tuned for updates! ## Introduction From 3eb98c61a0dc25cba32496f5d40daaf1a57320c4 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Mon, 7 Apr 2025 08:21:32 +0200 Subject: [PATCH 041/173] Add debug core SLO plugin Signed-off-by: Xabier Larrakoetxea --- internal/plugin/slo/core/debug_v1/plugin.go | 53 +++++++++++++ .../plugin/slo/core/debug_v1/plugin_test.go | 74 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 internal/plugin/slo/core/debug_v1/plugin.go create mode 100644 internal/plugin/slo/core/debug_v1/plugin_test.go diff --git a/internal/plugin/slo/core/debug_v1/plugin.go b/internal/plugin/slo/core/debug_v1/plugin.go new file mode 100644 index 00000000..dcc41f84 --- /dev/null +++ b/internal/plugin/slo/core/debug_v1/plugin.go @@ -0,0 +1,53 @@ +package plugin + +import ( + "context" + "encoding/json" + + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/core/debug/v1" +) + +type Config struct { + CustomMsg string `json:"msg,omitempty"` + ShowResult bool `json:"result,omitempty"` + ShowRequest bool `json:"request,omitempty"` +} + +func NewPlugin(configData json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + cfg := Config{} + err := json.Unmarshal(configData, &cfg) + if err != nil { + return nil, err + } + + return plugin{ + config: cfg, + appUtils: appUtils, + }, nil +} + +type plugin struct { + config Config + appUtils pluginslov1.AppUtils +} + +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + if p.config.CustomMsg != "" { + p.appUtils.Logger.Debugf("%s", p.config.CustomMsg) + } + + if p.config.ShowRequest { + p.appUtils.Logger.Debugf("%+v", *request) + } + + if p.config.ShowResult { + p.appUtils.Logger.Debugf("%+v", *result) + } + + return nil +} diff --git a/internal/plugin/slo/core/debug_v1/plugin_test.go b/internal/plugin/slo/core/debug_v1/plugin_test.go new file mode 100644 index 00000000..d00ecb36 --- /dev/null +++ b/internal/plugin/slo/core/debug_v1/plugin_test.go @@ -0,0 +1,74 @@ +package plugin_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + plugin "github.com/slok/sloth/internal/plugin/slo/core/debug_v1" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" +) + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + config json.RawMessage + req pluginslov1.Request + expRes pluginslov1.Result + expErr bool + }{ + "A log plugin should log.": { + config: []byte(`{"msg":"test"}`), + req: pluginslov1.Request{}, + expRes: pluginslov1.Result{}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: test.config}) + require.NoError(err) + + res := pluginslov1.Result{} + err = plugin.ProcessSLO(t.Context(), &test.req, &res) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, res) + } + }) + } +} + +func BenchmarkPluginYaegi(b *testing.B) { + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: []byte(`{}`)}) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPluginGo(b *testing.B) { + plugin, err := plugin.NewPlugin([]byte(`{}`), pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} From dd5ff30bbf4b48da25fa064b9fa7ff38c7554aab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 07:39:56 +0000 Subject: [PATCH 042/173] build(deps): bump github.com/prometheus/client_golang Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.1 to 1.22.0. - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.21.1...v1.22.0) --- updated-dependencies: - dependency-name: github.com/prometheus/client_golang dependency-version: 1.22.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 3 +-- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index f08f870d..55ecfa6f 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/oklog/run v1.1.0 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.81.0 github.com/prometheus-operator/prometheus-operator/pkg/client v0.81.0 - github.com/prometheus/client_golang v1.21.1 + github.com/prometheus/client_golang v1.22.0 github.com/prometheus/common v0.63.0 github.com/prometheus/prometheus v0.302.2-0.20250320225832-3d603d19575e github.com/sirupsen/logrus v1.9.3 @@ -52,7 +52,6 @@ require ( github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index 6dc15f8d..c5c811c0 100644 --- a/go.sum +++ b/go.sum @@ -144,8 +144,8 @@ github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.81.0 h github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.81.0/go.mod h1:YfnEQzw7tUQa0Sjiz8V6QFc6JUGE+i5wybsjc3EOKn8= github.com/prometheus-operator/prometheus-operator/pkg/client v0.81.0 h1:z8ETgiD2hThJi3+3S8eKAbC9/pwPq1kGt8HkeGlDstw= github.com/prometheus-operator/prometheus-operator/pkg/client v0.81.0/go.mod h1:EKK9z5OIxxwaCZmgaidiIfvy6mF7x8M3AJHmxwx+QB4= -github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= -github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= From 7ce6e538fbe6cd1f2a83aa7e62d3aa1a69358752 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Thu, 10 Apr 2025 08:51:08 +0200 Subject: [PATCH 043/173] Add support for SLO plugins on default (non-k8s) sloth prometheus service level spec Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 2 + examples/_gen/slo-plugin-getting-started.yml | 249 ++++++++++++++++++ examples/slo-plugin-getting-started.yml | 56 ++++ internal/storage/io/sloth.go | 23 +- internal/storage/io/sloth_test.go | 41 +++ pkg/prometheus/api/v1/README.md | 108 +++++--- pkg/prometheus/api/v1/v1.go | 81 ++++-- scripts/check/integration-test-cli.sh | 2 +- test/integration/prometheus/generate_test.go | 11 +- test/integration/prometheus/helpers.go | 6 +- .../{plugin => sli_plugins/plugin1}/plugin.go | 0 .../prometheus/slo_plugins/plugin1/plugin.go | 46 ++++ .../{in-plugin.yaml => in-sli-plugin.yaml} | 0 .../prometheus/testdata/in-slo-plugin.yaml | 33 +++ ...lugin.yaml.tpl => out-sli-plugin.yaml.tpl} | 0 .../testdata/out-slo-plugin.yaml.tpl | 177 +++++++++++++ 16 files changed, 768 insertions(+), 67 deletions(-) create mode 100644 examples/_gen/slo-plugin-getting-started.yml create mode 100644 examples/slo-plugin-getting-started.yml rename test/integration/prometheus/{plugin => sli_plugins/plugin1}/plugin.go (100%) create mode 100644 test/integration/prometheus/slo_plugins/plugin1/plugin.go rename test/integration/prometheus/testdata/{in-plugin.yaml => in-sli-plugin.yaml} (100%) create mode 100644 test/integration/prometheus/testdata/in-slo-plugin.yaml rename test/integration/prometheus/testdata/{out-plugin.yaml.tpl => out-sli-plugin.yaml.tpl} (100%) create mode 100644 test/integration/prometheus/testdata/out-slo-plugin.yaml.tpl diff --git a/CHANGELOG.md b/CHANGELOG.md index e8981ab6..cbaa3995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - Split image registry and repository in Helm chart +- Internally Sloth (not k8s) prometheusServiceLevel uses k8s `k8s.io/apimachinery/pkg/util/yaml` lib for unmarshaling YAML instead of `gopkg.in/yaml.v2`. ### Added @@ -13,6 +14,7 @@ - A new SLO rule generation plugin system has been added to be able to change/extend the SLO rule generation process. - SLO plugins can be loaded from FS directories recursively using `--slo-plugins-path` in the commands. - SLO plugins have a priority value to be able to order in the execution chain. +- Sloth regular (non-k8s) `prometheus/v1` API support for SLO plugins at SLO group level and per SLO level. ## [v0.12.0] - 2025-03-27 diff --git a/examples/_gen/slo-plugin-getting-started.yml b/examples/_gen/slo-plugin-getting-started.yml new file mode 100644 index 00000000..8fb10d75 --- /dev/null +++ b/examples/_gen/slo-plugin-getting-started.yml @@ -0,0 +1,249 @@ + +--- +# Code generated by Sloth (dev): https://github.com/slok/sloth. +# DO NOT EDIT. + +groups: +- name: sloth-slo-sli-recordings-myservice-requests-availability + rules: + - record: slo:sli_error:ratio_rate5m + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[5m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[5m]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 5m + tier: "2" + - record: slo:sli_error:ratio_rate30m + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[30m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[30m]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 30m + tier: "2" + - record: slo:sli_error:ratio_rate1h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 1h + tier: "2" + - record: slo:sli_error:ratio_rate2h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[2h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[2h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 2h + tier: "2" + - record: slo:sli_error:ratio_rate6h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[6h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[6h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 6h + tier: "2" + - record: slo:sli_error:ratio_rate1d + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1d]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 1d + tier: "2" + - record: slo:sli_error:ratio_rate3d + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[3d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[3d]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 3d + tier: "2" + - record: slo:sli_error:ratio_rate30d + expr: | + sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"}[30d]) + / ignoring (sloth_window) + count_over_time(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"}[30d]) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 30d + tier: "2" +- name: sloth-slo-meta-recordings-myservice-requests-availability + rules: + - record: slo:objective:ratio + expr: vector(0.9990000000000001) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + - record: slo:error_budget:ratio + expr: vector(1-0.9990000000000001) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + - record: slo:time_period:days + expr: vector(30) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + - record: slo:current_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + - record: slo:period_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate30d{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + - record: slo:period_error_budget_remaining:ratio + expr: 1 - slo:period_burn_rate:ratio{sloth_id="myservice-requests-availability", + sloth_service="myservice", sloth_slo="requests-availability"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + - record: sloth_slo_info + expr: vector(1) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_mode: cli-gen-prom + sloth_objective: "99.9" + sloth_service: myservice + sloth_slo: requests-availability + sloth_spec: prometheus/v1 + sloth_version: dev + tier: "2" +- name: sloth-slo-alerts-myservice-requests-availability + rules: + - alert: MyServiceHighErrorRate + expr: | + ( + max(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (14.4 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (14.4 * 0.0009999999999999432)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate30m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (6 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate6h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (6 * 0.0009999999999999432)) without (sloth_window) + ) + labels: + category: availability + routing_key: myteam + severity: pageteam + sloth_severity: page + annotations: + summary: High error rate on 'myservice' requests responses + title: (page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. + - alert: MyServiceHighErrorRate + expr: | + ( + max(slo:sli_error:ratio_rate2h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (3 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1d{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (3 * 0.0009999999999999432)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate6h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (1 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate3d{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (1 * 0.0009999999999999432)) without (sloth_window) + ) + labels: + category: availability + severity: slack + slack_channel: '#alerts-myteam' + sloth_severity: ticket + annotations: + summary: High error rate on 'myservice' requests responses + title: (ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. diff --git a/examples/slo-plugin-getting-started.yml b/examples/slo-plugin-getting-started.yml new file mode 100644 index 00000000..64d99264 --- /dev/null +++ b/examples/slo-plugin-getting-started.yml @@ -0,0 +1,56 @@ +version: "prometheus/v1" +service: "myservice" +labels: + owner: "myteam" + repo: "myorg/myservice" + tier: "2" +slo_plugins: + chain: + - id: "sloth.dev/core/debug/v1" + priority: 9999999 + config: {msg: "Plugin 99"} + - id: "sloth.dev/core/debug/v1" + priority: -999999 + config: {msg: "Plugin 0"} + +slos: + # We allow failing (5xx and 429) 1 request every 1000 requests (99.9%). + - name: "requests-availability" + objective: 99.9 + description: "Common SLO based on availability for HTTP request responses." + plugins: + chain: + - id: "sloth.dev/core/debug/v1" + priority: 1050 + config: {msg: "Plugin 5"} + - id: "sloth.dev/core/debug/v1" + priority: -1000 + config: {msg: "Plugin 1"} + - id: "sloth.dev/core/debug/v1" + priority: 1000 + config: {msg: "Plugin 4"} + - id: "sloth.dev/core/debug/v1" + priority: -200 + config: {msg: "Plugin 2"} + - id: "sloth.dev/core/debug/v1" + config: {msg: "Plugin 3"} + + sli: + events: + error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) + total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) + alerting: + name: MyServiceHighErrorRate + labels: + category: "availability" + annotations: + # Overwrite default Sloth SLO alert summmary on ticket and page alerts. + summary: "High error rate on 'myservice' requests responses" + page_alert: + labels: + severity: pageteam + routing_key: myteam + ticket_alert: + labels: + severity: "slack" + slack_channel: "#alerts-myteam" diff --git a/internal/storage/io/sloth.go b/internal/storage/io/sloth.go index 2e4041e7..41d1affc 100644 --- a/internal/storage/io/sloth.go +++ b/internal/storage/io/sloth.go @@ -6,7 +6,7 @@ import ( "regexp" "time" - "gopkg.in/yaml.v2" + "k8s.io/apimachinery/pkg/util/yaml" pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" "github.com/slok/sloth/pkg/common/model" @@ -70,7 +70,27 @@ func (l SlothPrometheusYAMLSpecLoader) LoadSpec(ctx context.Context, data []byte func (l SlothPrometheusYAMLSpecLoader) mapSpecToModel(ctx context.Context, spec prometheusv1.Spec) (*model.PromSLOGroup, error) { models := make([]model.PromSLO, 0, len(spec.SLOs)) + + // Get group plugins if any. + var groupSLOPlugins []model.PromSLOPluginMetadata + for _, plugin := range spec.SLOPlugins.Chain { + groupSLOPlugins = append(groupSLOPlugins, model.PromSLOPluginMetadata{ + ID: plugin.ID, + Config: plugin.Config, + Priority: plugin.Priority, + }) + } + for _, specSLO := range spec.SLOs { + plugins := append([]model.PromSLOPluginMetadata{}, groupSLOPlugins...) // Add group plugins if any. + for _, plugin := range specSLO.Plugins.Chain { + plugins = append(plugins, model.PromSLOPluginMetadata{ + ID: plugin.ID, + Config: plugin.Config, + Priority: plugin.Priority, + }) + } + slo := model.PromSLO{ ID: fmt.Sprintf("%s-%s", spec.Service, specSLO.Name), Name: specSLO.Name, @@ -81,6 +101,7 @@ func (l SlothPrometheusYAMLSpecLoader) mapSpecToModel(ctx context.Context, spec Labels: utilsdata.MergeLabels(spec.Labels, specSLO.Labels), PageAlertMeta: model.PromAlertMeta{Disable: true}, TicketAlertMeta: model.PromAlertMeta{Disable: true}, + Plugins: model.SLOPlugins{Plugins: plugins}, } // Set SLIs. diff --git a/internal/storage/io/sloth_test.go b/internal/storage/io/sloth_test.go index a3ceeebb..131db1e4 100644 --- a/internal/storage/io/sloth_test.go +++ b/internal/storage/io/sloth_test.go @@ -2,6 +2,7 @@ package io_test import ( "context" + "encoding/json" "fmt" "testing" "time" @@ -159,6 +160,7 @@ slos: Service: "test-svc", TimeWindow: 30 * 24 * time.Hour, Labels: map[string]string{"gk1": "gv1"}, + Plugins: model.SLOPlugins{Plugins: []model.PromSLOPluginMetadata{}}, SLI: model.PromSLI{ Raw: &model.PromSLIRaw{ ErrorRatioQuery: `plugin_raw_expr{service="test-svc",slo="slo-test",objective="99.000000",gk1="gv1",k1="v1",k2="true"}`, @@ -217,6 +219,7 @@ slos: Service: "test-svc", TimeWindow: 28 * 24 * time.Hour, Labels: map[string]string{"gk1": "gv1"}, + Plugins: model.SLOPlugins{Plugins: []model.PromSLOPluginMetadata{}}, SLI: model.PromSLI{ Raw: &model.PromSLIRaw{ ErrorRatioQuery: `test_expr_ratio_2`, @@ -253,6 +256,13 @@ version: "prometheus/v1" service: "test-svc" labels: owner: "myteam" +slo_plugins: + chain: + - id: test_plugin0 + priority: -100 + config: {"k1": 42} + - id: test_plugin2 + config: {"k1": {"k2": "v2"}} slos: - name: "slo1" labels: @@ -288,6 +298,13 @@ slos: sli: raw: error_ratio_query: test_expr_ratio_2 + plugins: + chain: + - id: test_plugin1 + priority: 100 + config: + k1: v1 + k2: true alerting: page_alert: disable: true @@ -338,6 +355,12 @@ slos: "runbook": "http://whatever.com", }, }, + Plugins: model.SLOPlugins{ + Plugins: []model.PromSLOPluginMetadata{ + {ID: "test_plugin0", Priority: -100, Config: json.RawMessage([]byte(`{"k1":42}`))}, + {ID: "test_plugin2", Config: json.RawMessage([]byte(`{"k1":{"k2":"v2"}}`))}, + }, + }, }, { ID: "test-svc-slo2", @@ -356,12 +379,25 @@ slos: }, PageAlertMeta: model.PromAlertMeta{Disable: true}, TicketAlertMeta: model.PromAlertMeta{Disable: true}, + Plugins: model.SLOPlugins{ + Plugins: []model.PromSLOPluginMetadata{ + {ID: "test_plugin0", Priority: -100, Config: json.RawMessage([]byte(`{"k1":42}`))}, + {ID: "test_plugin2", Config: json.RawMessage([]byte(`{"k1":{"k2":"v2"}}`))}, + {ID: "test_plugin1", Priority: 100, Config: json.RawMessage([]byte(`{"k1":"v1","k2":true}`))}, + }, + }, }, }, OriginalSource: model.PromSLOGroupSource{SlothV1: &v1.Spec{ Version: "prometheus/v1", Service: "test-svc", Labels: map[string]string{"owner": "myteam"}, + SLOPlugins: v1.SLOPlugins{ + Chain: []v1.SLOPlugin{ + {ID: "test_plugin0", Priority: -100, Config: json.RawMessage([]byte(`{"k1":42}`))}, + {ID: "test_plugin2", Config: json.RawMessage([]byte(`{"k1":{"k2":"v2"}}`))}, + }, + }, SLOs: []v1.SLO{ { Name: "slo1", @@ -400,6 +436,11 @@ slos: PageAlert: v1.Alert{Disable: true}, TicketAlert: v1.Alert{Disable: true}, }, + Plugins: v1.SLOPlugins{ + Chain: []v1.SLOPlugin{ + {ID: "test_plugin1", Priority: 100, Config: []byte(`{"k1":"v1","k2":true}`)}, + }, + }, }, }, }}, diff --git a/pkg/prometheus/api/v1/README.md b/pkg/prometheus/api/v1/README.md index 43e4f0f2..a17a0741 100755 --- a/pkg/prometheus/api/v1/README.md +++ b/pkg/prometheus/api/v1/README.md @@ -73,6 +73,8 @@ slos: - [type SLIPlugin](<#SLIPlugin>) - [type SLIRaw](<#SLIRaw>) - [type SLO](<#SLO>) +- [type SLOPlugin](<#SLOPlugin>) +- [type SLOPlugins](<#SLOPlugins>) - [type Spec](<#Spec>) @@ -85,7 +87,7 @@ const Version = "prometheus/v1" ``` -## type [Alert]() +## type [Alert]() Alert configures specific SLO alert. @@ -93,38 +95,38 @@ Alert configures specific SLO alert. type Alert struct { // Disable disables the alert and makes Sloth not generating this alert. This // can be helpful for example to disable ticket(warning) alerts. - Disable bool `yaml:"disable,omitempty"` + Disable bool `json:"disable,omitempty"` // Labels are the Prometheus labels for the specific alert. For example can be // useful to route the Page alert to specific Slack channel. - Labels map[string]string `yaml:"labels,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // Annotations are the Prometheus annotations for the specific alert. - Annotations map[string]string `yaml:"annotations,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` } ``` -## type [Alerting]() +## type [Alerting]() Alerting wraps all the configuration required by the SLO alerts. ```go type Alerting struct { // Name is the name used by the alerts generated for this SLO. - Name string `yaml:"name" validate:"required"` + Name string `json:"name" validate:"required"` // Labels are the Prometheus labels that will have all the alerts generated by this SLO. - Labels map[string]string `yaml:"labels,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // Annotations are the Prometheus annotations that will have all the alerts generated by // this SLO. - Annotations map[string]string `yaml:"annotations,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` // Page alert refers to the critical alert (check multiwindow-multiburn alerts). - PageAlert Alert `yaml:"page_alert,omitempty"` + PageAlert Alert `json:"page_alert,omitempty"` // TicketAlert alert refers to the warning alert (check multiwindow-multiburn alerts). - TicketAlert Alert `yaml:"ticket_alert,omitempty"` + TicketAlert Alert `json:"ticket_alert,omitempty"` } ``` -## type [SLI]() +## type [SLI]() SLI will tell what is good or bad for the SLO. All SLIs will be get based on time windows, that's why Sloth needs the queries to use \`\{\{.window\}\}\` template variable. @@ -133,16 +135,16 @@ Only one of the SLI types can be used. ```go type SLI struct { // Raw is the raw SLI type. - Raw *SLIRaw `yaml:"raw,omitempty"` + Raw *SLIRaw `json:"raw,omitempty"` // Events is the events SLI type. - Events *SLIEvents `yaml:"events,omitempty"` + Events *SLIEvents `json:"events,omitempty"` // Plugin is the pluggable SLI type. - Plugin *SLIPlugin `yaml:"plugin,omitempty"` + Plugin *SLIPlugin `json:"plugin,omitempty"` } ``` -## type [SLIEvents]() +## type [SLIEvents]() SLIEvents is an SLI that is calculated as the division of bad events and total events, giving a ratio SLI. Normally this is the most common ratio type. @@ -151,81 +153,119 @@ type SLIEvents struct { // ErrorQuery is a Prometheus query that will get the number/count of events // that we consider that are bad for the SLO (e.g "http 5xx", "latency > 250ms"...). // Requires the usage of `{{.window}}` template variable. - ErrorQuery string `yaml:"error_query"` + ErrorQuery string `json:"error_query"` // TotalQuery is a Prometheus query that will get the total number/count of events // for the SLO (e.g "all http requests"...). // Requires the usage of `{{.window}}` template variable. - TotalQuery string `yaml:"total_query"` + TotalQuery string `json:"total_query"` } ``` -## type [SLIPlugin]() +## type [SLIPlugin]() SLIPlugin will use the SLI returned by the SLI plugin selected along with the options. ```go type SLIPlugin struct { // Name is the name of the plugin that needs to load. - ID string `yaml:"id"` + ID string `json:"id"` // Options are the options used for the plugin. - Options map[string]string `yaml:"options"` + Options map[string]string `json:"options"` } ``` -## type [SLIRaw]() +## type [SLIRaw]() SLIRaw is a error ratio SLI already calculated. Normally this will be used when the SLI is already calculated by other recording rule, system... ```go type SLIRaw struct { // ErrorRatioQuery is a Prometheus query that will get the raw error ratio (0-1) for the SLO. - ErrorRatioQuery string `yaml:"error_ratio_query"` + ErrorRatioQuery string `json:"error_ratio_query"` } ``` -## type [SLO]() +## type [SLO]() SLO is the configuration/declaration of the service level objective of a service. ```go type SLO struct { // Name is the name of the SLO. - Name string `yaml:"name"` + Name string `json:"name"` // Description is the description of the SLO. - Description string `yaml:"description,omitempty"` + Description string `json:"description,omitempty"` // Objective is target of the SLO the percentage (0, 100] (e.g 99.9). - Objective float64 `yaml:"objective"` + Objective float64 `json:"objective"` + // Plugins will be added along the group SLO plugins declared in the spec root level + // and Sloth default plugins. + Plugins SLOPlugins `json:"plugins,omitempty"` // Labels are the Prometheus labels that will have all the recording and // alerting rules for this specific SLO. These labels are merged with the // previous level labels. - Labels map[string]string `yaml:"labels,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // SLI is the indicator (service level indicator) for this specific SLO. - SLI SLI `yaml:"sli"` + SLI SLI `json:"sli"` // Alerting is the configuration with all the things related with the SLO // alerts. - Alerting Alerting `yaml:"alerting"` + Alerting Alerting `json:"alerting"` +} +``` + + +## type [SLOPlugin]() + +SLOPlugin is a plugin that will be used on the chain of plugins for the SLO generation. + +```go +type SLOPlugin struct { + // ID is the ID of the plugin to load . + ID string `json:"id"` + // Config is the configuration of the plugin creation. + Config json.RawMessage `json:"config,omitempty"` + // Priority is the priority of the plugin in the chain. The lower the number + // the higher the priority. The first plugin will be the one with the lowest + // priority. + // The default plugins loaded by Sloth use `0` priority. If you want to + // execute plugins before the default ones, you can use negative priority. + // It is recommended to use round gaps of numbers like 10, 100, 1000, -200, -1000... + Priority int `json:"priority,omitempty"` +} +``` + + +## type [SLOPlugins]() + +SLOPlugins are the list plugins that will be used on the process of SLOs for the rules generation. + +```go +type SLOPlugins struct { + // chain ths the list of plugin chain to add to the SLO generation. + Chain []SLOPlugin `json:"chain"` } ``` -## type [Spec]() +## type [Spec]() Spec represents the root type of the SLOs declaration specification. ```go type Spec struct { // Version is the version of the spec. - Version string `yaml:"version"` + Version string `json:"version"` // Service is the application of the SLOs. - Service string `yaml:"service"` + Service string `json:"service"` // Labels are the Prometheus labels that will have all the recording // and alerting rules generated for the service SLOs. - Labels map[string]string `yaml:"labels,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + // SLOPlugins will be added to the SLO generation plugin chain of all SLOs. + SLOPlugins SLOPlugins `json:"slo_plugins,omitempty"` // SLOs are the SLOs of the service. - SLOs []SLO `yaml:"slos,omitempty"` + SLOs []SLO `json:"slos,omitempty"` } ``` diff --git a/pkg/prometheus/api/v1/v1.go b/pkg/prometheus/api/v1/v1.go index d81d367d..30286660 100644 --- a/pkg/prometheus/api/v1/v1.go +++ b/pkg/prometheus/api/v1/v1.go @@ -54,6 +54,8 @@ // disable: true package v1 +import "encoding/json" + const Version = "prometheus/v1" //go:generate gomarkdoc -o ./README.md ./ @@ -61,34 +63,39 @@ const Version = "prometheus/v1" // Spec represents the root type of the SLOs declaration specification. type Spec struct { // Version is the version of the spec. - Version string `yaml:"version"` + Version string `json:"version"` // Service is the application of the SLOs. - Service string `yaml:"service"` + Service string `json:"service"` // Labels are the Prometheus labels that will have all the recording // and alerting rules generated for the service SLOs. - Labels map[string]string `yaml:"labels,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + // SLOPlugins will be added to the SLO generation plugin chain of all SLOs. + SLOPlugins SLOPlugins `json:"slo_plugins,omitempty"` // SLOs are the SLOs of the service. - SLOs []SLO `yaml:"slos,omitempty"` + SLOs []SLO `json:"slos,omitempty"` } // SLO is the configuration/declaration of the service level objective of // a service. type SLO struct { // Name is the name of the SLO. - Name string `yaml:"name"` + Name string `json:"name"` // Description is the description of the SLO. - Description string `yaml:"description,omitempty"` + Description string `json:"description,omitempty"` // Objective is target of the SLO the percentage (0, 100] (e.g 99.9). - Objective float64 `yaml:"objective"` + Objective float64 `json:"objective"` + // Plugins will be added along the group SLO plugins declared in the spec root level + // and Sloth default plugins. + Plugins SLOPlugins `json:"plugins,omitempty"` // Labels are the Prometheus labels that will have all the recording and // alerting rules for this specific SLO. These labels are merged with the // previous level labels. - Labels map[string]string `yaml:"labels,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // SLI is the indicator (service level indicator) for this specific SLO. - SLI SLI `yaml:"sli"` + SLI SLI `json:"sli"` // Alerting is the configuration with all the things related with the SLO // alerts. - Alerting Alerting `yaml:"alerting"` + Alerting Alerting `json:"alerting"` } // SLI will tell what is good or bad for the SLO. @@ -98,18 +105,18 @@ type SLO struct { // Only one of the SLI types can be used. type SLI struct { // Raw is the raw SLI type. - Raw *SLIRaw `yaml:"raw,omitempty"` + Raw *SLIRaw `json:"raw,omitempty"` // Events is the events SLI type. - Events *SLIEvents `yaml:"events,omitempty"` + Events *SLIEvents `json:"events,omitempty"` // Plugin is the pluggable SLI type. - Plugin *SLIPlugin `yaml:"plugin,omitempty"` + Plugin *SLIPlugin `json:"plugin,omitempty"` } // SLIRaw is a error ratio SLI already calculated. Normally this will be used when the SLI // is already calculated by other recording rule, system... type SLIRaw struct { // ErrorRatioQuery is a Prometheus query that will get the raw error ratio (0-1) for the SLO. - ErrorRatioQuery string `yaml:"error_ratio_query"` + ErrorRatioQuery string `json:"error_ratio_query"` } // SLIEvents is an SLI that is calculated as the division of bad events and total events, giving @@ -118,44 +125,66 @@ type SLIEvents struct { // ErrorQuery is a Prometheus query that will get the number/count of events // that we consider that are bad for the SLO (e.g "http 5xx", "latency > 250ms"...). // Requires the usage of `{{.window}}` template variable. - ErrorQuery string `yaml:"error_query"` + ErrorQuery string `json:"error_query"` // TotalQuery is a Prometheus query that will get the total number/count of events // for the SLO (e.g "all http requests"...). // Requires the usage of `{{.window}}` template variable. - TotalQuery string `yaml:"total_query"` + TotalQuery string `json:"total_query"` } // SLIPlugin will use the SLI returned by the SLI plugin selected along with the options. type SLIPlugin struct { // Name is the name of the plugin that needs to load. - ID string `yaml:"id"` + ID string `json:"id"` // Options are the options used for the plugin. - Options map[string]string `yaml:"options"` + Options map[string]string `json:"options"` } // Alerting wraps all the configuration required by the SLO alerts. type Alerting struct { // Name is the name used by the alerts generated for this SLO. - Name string `yaml:"name" validate:"required"` + Name string `json:"name" validate:"required"` // Labels are the Prometheus labels that will have all the alerts generated by this SLO. - Labels map[string]string `yaml:"labels,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // Annotations are the Prometheus annotations that will have all the alerts generated by // this SLO. - Annotations map[string]string `yaml:"annotations,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` // Page alert refers to the critical alert (check multiwindow-multiburn alerts). - PageAlert Alert `yaml:"page_alert,omitempty"` + PageAlert Alert `json:"page_alert,omitempty"` // TicketAlert alert refers to the warning alert (check multiwindow-multiburn alerts). - TicketAlert Alert `yaml:"ticket_alert,omitempty"` + TicketAlert Alert `json:"ticket_alert,omitempty"` } // Alert configures specific SLO alert. type Alert struct { // Disable disables the alert and makes Sloth not generating this alert. This // can be helpful for example to disable ticket(warning) alerts. - Disable bool `yaml:"disable,omitempty"` + Disable bool `json:"disable,omitempty"` // Labels are the Prometheus labels for the specific alert. For example can be // useful to route the Page alert to specific Slack channel. - Labels map[string]string `yaml:"labels,omitempty"` + Labels map[string]string `json:"labels,omitempty"` // Annotations are the Prometheus annotations for the specific alert. - Annotations map[string]string `yaml:"annotations,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` +} + +// SLOPlugins are the list plugins that will be used on the process of SLOs for the +// rules generation. +type SLOPlugins struct { + // chain ths the list of plugin chain to add to the SLO generation. + Chain []SLOPlugin `json:"chain"` +} + +// SLOPlugin is a plugin that will be used on the chain of plugins for the SLO generation. +type SLOPlugin struct { + // ID is the ID of the plugin to load . + ID string `json:"id"` + // Config is the configuration of the plugin creation. + Config json.RawMessage `json:"config,omitempty"` + // Priority is the priority of the plugin in the chain. The lower the number + // the higher the priority. The first plugin will be the one with the lowest + // priority. + // The default plugins loaded by Sloth use `0` priority. If you want to + // execute plugins before the default ones, you can use negative priority. + // It is recommended to use round gaps of numbers like 10, 100, 1000, -200, -1000... + Priority int `json:"priority,omitempty"` } diff --git a/scripts/check/integration-test-cli.sh b/scripts/check/integration-test-cli.sh index 442b49ee..d53df057 100755 --- a/scripts/check/integration-test-cli.sh +++ b/scripts/check/integration-test-cli.sh @@ -3,4 +3,4 @@ set -o errexit set -o nounset -go test -race -tags='integration' -v ./test/integration/prometheus/... \ No newline at end of file +go test -race -tags='integration' -v ./test/integration/prometheus/... diff --git a/test/integration/prometheus/generate_test.go b/test/integration/prometheus/generate_test.go index 0b2f28eb..ddfabc74 100644 --- a/test/integration/prometheus/generate_test.go +++ b/test/integration/prometheus/generate_test.go @@ -75,9 +75,14 @@ func TestPrometheusGenerate(t *testing.T) { expOut: expectLoader.mustLoadExp("./testdata/out-base-extra-labels.yaml.tpl"), }, - "Generate with plugins should generate the correct rules for all the SLOs.": { - genCmdArgs: "--input ./testdata/in-plugin.yaml", - expOut: expectLoader.mustLoadExp("./testdata/out-plugin.yaml.tpl"), + "Generate with SLI plugins should generate the correct rules for all the SLOs.": { + genCmdArgs: "--input ./testdata/in-sli-plugin.yaml", + expOut: expectLoader.mustLoadExp("./testdata/out-sli-plugin.yaml.tpl"), + }, + + "Generate with SLO plugins should generate the correct rules for all the SLOs.": { + genCmdArgs: "--input ./testdata/in-slo-plugin.yaml", + expOut: expectLoader.mustLoadExp("./testdata/out-slo-plugin.yaml.tpl"), }, "Generate using multifile YAML in single file should generate the correct rules for all the SLOs.": { diff --git a/test/integration/prometheus/helpers.go b/test/integration/prometheus/helpers.go index cfa4f00f..8c0371a7 100644 --- a/test/integration/prometheus/helpers.go +++ b/test/integration/prometheus/helpers.go @@ -48,7 +48,8 @@ func NewConfig(t *testing.T) Config { func RunSlothGenerate(ctx context.Context, config Config, cmdArgs string) (stdout, stderr []byte, err error) { env := []string{ - fmt.Sprintf("SLOTH_SLI_PLUGINS_PATH=%s", "./"), + fmt.Sprintf("SLOTH_SLI_PLUGINS_PATH=%s", "./sli_plugins"), + fmt.Sprintf("SLOTH_SLO_PLUGINS_PATH=%s", "./slo_plugins"), } return testutils.RunSloth(ctx, env, config.Binary, fmt.Sprintf("generate %s", cmdArgs), true) @@ -56,7 +57,8 @@ func RunSlothGenerate(ctx context.Context, config Config, cmdArgs string) (stdou func RunSlothValidate(ctx context.Context, config Config, cmdArgs string) (stdout, stderr []byte, err error) { env := []string{ - fmt.Sprintf("SLOTH_SLI_PLUGINS_PATH=%s", "./"), + fmt.Sprintf("SLOTH_SLI_PLUGINS_PATH=%s", "./sli_plugins"), + fmt.Sprintf("SLOTH_SLO_PLUGINS_PATH=%s", "./slo_plugins"), } return testutils.RunSloth(ctx, env, config.Binary, fmt.Sprintf("validate %s", cmdArgs), true) diff --git a/test/integration/prometheus/plugin/plugin.go b/test/integration/prometheus/sli_plugins/plugin1/plugin.go similarity index 100% rename from test/integration/prometheus/plugin/plugin.go rename to test/integration/prometheus/sli_plugins/plugin1/plugin.go diff --git a/test/integration/prometheus/slo_plugins/plugin1/plugin.go b/test/integration/prometheus/slo_plugins/plugin1/plugin.go new file mode 100644 index 00000000..97aaf03e --- /dev/null +++ b/test/integration/prometheus/slo_plugins/plugin1/plugin.go @@ -0,0 +1,46 @@ +package plugin + +import ( + "context" + "encoding/json" + + utilsdata "github.com/slok/sloth/pkg/common/utils/data" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "integration-tests/plugin1" +) + +type Config struct { + Labels map[string]string `json:"labels,omitempty"` +} + +func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + cfg := Config{} + err := json.Unmarshal(configData, &cfg) + if err != nil { + return nil, err + } + + return plugin{ + config: cfg, + }, nil +} + +type plugin struct { + config Config +} + +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + for i, r := range result.SLORules.MetadataRecRules.Rules { + if r.Record == "sloth_slo_info" { + r.Labels = utilsdata.MergeLabels(r.Labels, p.config.Labels) + result.SLORules.MetadataRecRules.Rules[i] = r + break + } + } + + return nil +} diff --git a/test/integration/prometheus/testdata/in-plugin.yaml b/test/integration/prometheus/testdata/in-sli-plugin.yaml similarity index 100% rename from test/integration/prometheus/testdata/in-plugin.yaml rename to test/integration/prometheus/testdata/in-sli-plugin.yaml diff --git a/test/integration/prometheus/testdata/in-slo-plugin.yaml b/test/integration/prometheus/testdata/in-slo-plugin.yaml new file mode 100644 index 00000000..6641621b --- /dev/null +++ b/test/integration/prometheus/testdata/in-slo-plugin.yaml @@ -0,0 +1,33 @@ +version: "prometheus/v1" +service: "svc01" +labels: + owner: myteam + tier: "2" +slo_plugins: + chain: + - id: "integration-tests/plugin1" + priority: 9999999 + config: {labels: {"k1": "v1", "k2": "v2"}} + - id: "integration-tests/plugin1" + priority: -999999 + config: {labels: {"k3": "v3"}} # These should be replaced because is before defaults +slos: + - name: "slo1" + objective: 99.9 + description: "This is SLO 01." + sli: + events: + error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) + total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) + plugins: + chain: + - id: "integration-tests/plugin1" + config: {labels: {"k4": "v4"}} + - id: "integration-tests/plugin1" + priority: 1000 + config: {labels: {"k2": "v0", "k5": "v5"}} # k2 should be replaced by a (9999999 priority) plugin. + alerting: + page_alert: + disable: true + ticket_alert: + disable: true diff --git a/test/integration/prometheus/testdata/out-plugin.yaml.tpl b/test/integration/prometheus/testdata/out-sli-plugin.yaml.tpl similarity index 100% rename from test/integration/prometheus/testdata/out-plugin.yaml.tpl rename to test/integration/prometheus/testdata/out-sli-plugin.yaml.tpl diff --git a/test/integration/prometheus/testdata/out-slo-plugin.yaml.tpl b/test/integration/prometheus/testdata/out-slo-plugin.yaml.tpl new file mode 100644 index 00000000..501b1964 --- /dev/null +++ b/test/integration/prometheus/testdata/out-slo-plugin.yaml.tpl @@ -0,0 +1,177 @@ + +--- +# Code generated by Sloth ({{ .version }}): https://github.com/slok/sloth. +# DO NOT EDIT. + +groups: +- name: sloth-slo-sli-recordings-svc01-slo1 + rules: + - record: slo:sli_error:ratio_rate5m + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[5m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[5m]))) + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 5m + tier: "2" + - record: slo:sli_error:ratio_rate30m + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[30m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[30m]))) + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 30m + tier: "2" + - record: slo:sli_error:ratio_rate1h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1h]))) + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 1h + tier: "2" + - record: slo:sli_error:ratio_rate2h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[2h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[2h]))) + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 2h + tier: "2" + - record: slo:sli_error:ratio_rate6h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[6h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[6h]))) + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 6h + tier: "2" + - record: slo:sli_error:ratio_rate1d + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1d]))) + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 1d + tier: "2" + - record: slo:sli_error:ratio_rate3d + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[3d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[3d]))) + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 3d + tier: "2" + - record: slo:sli_error:ratio_rate30d + expr: | + sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"}[30d]) + / ignoring (sloth_window) + count_over_time(slo:sli_error:ratio_rate5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"}[30d]) + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 30d + tier: "2" +- name: sloth-slo-meta-recordings-svc01-slo1 + rules: + - record: slo:objective:ratio + expr: vector(0.9990000000000001) + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + tier: "2" + - record: slo:error_budget:ratio + expr: vector(1-0.9990000000000001) + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + tier: "2" + - record: slo:time_period:days + expr: vector(30) + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + tier: "2" + - record: slo:current_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + tier: "2" + - record: slo:period_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate30d{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + tier: "2" + - record: slo:period_error_budget_remaining:ratio + expr: 1 - slo:period_burn_rate:ratio{sloth_id="svc01-slo1", sloth_service="svc01", + sloth_slo="slo1"} + labels: + owner: myteam + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + tier: "2" + - record: sloth_slo_info + expr: vector(1) + labels: + k1: v1 + k2: v2 + k4: v4 + k5: v5 + owner: myteam + sloth_id: svc01-slo1 + sloth_mode: cli-gen-prom + sloth_objective: "99.9" + sloth_service: svc01 + sloth_slo: slo1 + sloth_spec: prometheus/v1 + sloth_version: {{ .version }} + tier: "2" From a9bd60245be6cf08afbdf44c6fbdc00454d24f49 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Thu, 10 Apr 2025 18:16:27 +0200 Subject: [PATCH 044/173] Move SLO validation to an SLO plugin Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 3 +- cmd/sloth/commands/generate.go | 11 + cmd/sloth/commands/k8scontroller.go | 11 + internal/app/generate/generate.go | 39 +- internal/app/generate/generate_test.go | 25 + .../plugin/slo/core/validate_v1/plugin.go | 34 ++ .../slo/core/validate_v1/plugin_test.go | 134 ++++++ pkg/common/model/slo_prometheus.go | 28 +- pkg/common/model/slo_prometheus_test.go | 432 +++++++++--------- 9 files changed, 460 insertions(+), 257 deletions(-) create mode 100644 internal/plugin/slo/core/validate_v1/plugin.go create mode 100644 internal/plugin/slo/core/validate_v1/plugin_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index cbaa3995..9c009235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ ### Changed - Split image registry and repository in Helm chart -- Internally Sloth (not k8s) prometheusServiceLevel uses k8s `k8s.io/apimachinery/pkg/util/yaml` lib for unmarshaling YAML instead of `gopkg.in/yaml.v2`. +- (BREAKING) Internally Sloth (not k8s) prometheusServiceLevel uses k8s `k8s.io/apimachinery/pkg/util/yaml` lib for unmarshaling YAML instead of `gopkg.in/yaml.v2`. +- Core SLO validation and SLO rules generation migrated to SLO plugins. ### Added diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 981210ee..5bf337a6 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -23,6 +23,7 @@ import ( plugincorealertrulesv1 "github.com/slok/sloth/internal/plugin/slo/core/alert_rules_v1" plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" + plugincorevalidatev1 "github.com/slok/sloth/internal/plugin/slo/core/validate_v1" "github.com/slok/sloth/internal/storage" storagefs "github.com/slok/sloth/internal/storage/fs" storageio "github.com/slok/sloth/internal/storage/io" @@ -450,6 +451,15 @@ func (g generator) generateRules(ctx context.Context, info model.Info, slos mode metaRuleGen = metadataPlugin } + validatePlugin, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincorevalidatev1.NewPlugin, + g.logger.WithValues(log.Kv{"plugin": plugincorevalidatev1.PluginID}), + nil, + ) + if err != nil { + return nil, fmt.Errorf("could not create SLO validate plugin: %w", err) + } + // Disable alert rules if required. var alertRuleGen generate.SLOProcessor = generate.NoopPlugin if !g.disableAlerts { @@ -470,6 +480,7 @@ func (g generator) generateRules(ctx context.Context, info model.Info, slos mode SLIRulesGenSLOPlugin: sliRuleGen, MetadataRulesGenSLOPlugin: metaRuleGen, AlertRulesGenSLOPlugin: alertRuleGen, + ValidateSLOPlugin: validatePlugin, SLOPluginGetter: g.sloPluginRepo, Logger: g.logger, }) diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index 3f101798..e57a834c 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -36,6 +36,7 @@ import ( plugincorealertrulesv1 "github.com/slok/sloth/internal/plugin/slo/core/alert_rules_v1" plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" + plugincorevalidatev1 "github.com/slok/sloth/internal/plugin/slo/core/validate_v1" "github.com/slok/sloth/internal/storage" storageio "github.com/slok/sloth/internal/storage/io" storagek8s "github.com/slok/sloth/internal/storage/k8s" @@ -339,12 +340,22 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error return fmt.Errorf("could not create alert rules plugin: %w", err) } + validatePlugin, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincorevalidatev1.NewPlugin, + logger.WithValues(log.Kv{"plugin": plugincorevalidatev1.PluginID}), + nil, + ) + if err != nil { + return fmt.Errorf("could not create SLO validate plugin: %w", err) + } + // Create the generate app service (the one that the CLIs use). generator, err := generate.NewService(generate.ServiceConfig{ AlertGenerator: alert.NewGenerator(windowsRepo), SLIRulesGenSLOPlugin: sliRuleGen, MetadataRulesGenSLOPlugin: metaRuleGen, AlertRulesGenSLOPlugin: alertRuleGen, + ValidateSLOPlugin: validatePlugin, SLOPluginGetter: pluginSLORepo, Logger: generatorLogger{Logger: logger}, }) diff --git a/internal/app/generate/generate.go b/internal/app/generate/generate.go index bbde914d..273d1a48 100644 --- a/internal/app/generate/generate.go +++ b/internal/app/generate/generate.go @@ -10,8 +10,9 @@ import ( "github.com/slok/sloth/internal/log" plugincorealertrulesv1 "github.com/slok/sloth/internal/plugin/slo/core/alert_rules_v1" plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" - plugincorenoopsv1 "github.com/slok/sloth/internal/plugin/slo/core/noop_v1" + plugincorenoopv1 "github.com/slok/sloth/internal/plugin/slo/core/noop_v1" plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" + plugincorevalidatev1 "github.com/slok/sloth/internal/plugin/slo/core/validate_v1" pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" commonerrors "github.com/slok/sloth/pkg/common/errors" "github.com/slok/sloth/pkg/common/model" @@ -20,7 +21,7 @@ import ( // Default plugins. var ( - NoopPlugin, _ = NewSLOProcessorFromSLOPluginV1(plugincorenoopsv1.NewPlugin, log.Noop, nil) + NoopPlugin, _ = NewSLOProcessorFromSLOPluginV1(plugincorenoopv1.NewPlugin, log.Noop, nil) ) type noopSLOPluginGetter bool @@ -41,6 +42,7 @@ type ServiceConfig struct { SLIRulesGenSLOPlugin SLOProcessor AlertRulesGenSLOPlugin SLOProcessor MetadataRulesGenSLOPlugin SLOProcessor + ValidateSLOPlugin SLOProcessor SLOPluginGetter SLOPluginGetter Logger log.Logger } @@ -94,6 +96,18 @@ func (c *ServiceConfig) defaults() error { c.MetadataRulesGenSLOPlugin = plugin } + if c.ValidateSLOPlugin == nil { + plugin, err := NewSLOProcessorFromSLOPluginV1( + plugincorevalidatev1.NewPlugin, + c.Logger.WithValues(log.Kv{"plugin": plugincorevalidatev1.PluginID}), + nil, + ) + if err != nil { + return fmt.Errorf("could not create SLO validate plugin: %w", err) + } + c.ValidateSLOPlugin = plugin + } + return nil } @@ -121,6 +135,7 @@ func NewService(config ServiceConfig) (*Service, error) { alertGen: config.AlertGenerator, sloPluginGetter: config.SLOPluginGetter, defaultPlugins: []SLOProcessor{ + config.ValidateSLOPlugin, config.SLIRulesGenSLOPlugin, config.AlertRulesGenSLOPlugin, config.MetadataRulesGenSLOPlugin, @@ -148,7 +163,7 @@ type Response struct { } func (s Service) Generate(ctx context.Context, r Request) (*Response, error) { - err := r.SLOGroup.Validate() + err := s.validateSLOGroup(r.SLOGroup) if err != nil { return nil, fmt.Errorf("invalid SLO group: %w", err) } @@ -238,3 +253,21 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, slo model.Pro return &res.SLORules, nil } + +func (s Service) validateSLOGroup(sloGroup model.PromSLOGroup) error { + if len(sloGroup.SLOs) == 0 { + return fmt.Errorf("at least one SLO is required") + } + + // Check SLO IDs not repeated. + sloIDs := map[string]struct{}{} + for _, slo := range sloGroup.SLOs { + _, ok := sloIDs[slo.ID] + if ok { + return fmt.Errorf("SLO ID %q is repeated", slo.ID) + } + sloIDs[slo.ID] = struct{}{} + } + + return nil +} diff --git a/internal/app/generate/generate_test.go b/internal/app/generate/generate_test.go index d7ab45ba..5dcb0178 100644 --- a/internal/app/generate/generate_test.go +++ b/internal/app/generate/generate_test.go @@ -52,6 +52,31 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { expErr: true, }, + "Having invalid SLOs should error.": { + mocks: func(mspg *generatemock.SLOPluginGetter) {}, + req: generate.Request{ + SLOGroup: model.PromSLOGroup{SLOs: []model.PromSLO{ + { + ID: "test-id", + Name: "test-name", + Service: "test-svc", + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `rate(my_metric{error="true"}[{{.window}}])`, + TotalQuery: `rate(my_metric[{{.window}}])`, + }, + }, + TimeWindow: 30 * 24 * time.Hour, + Objective: 101, // This is wrong. + Labels: map[string]string{"test_label": "label_1"}, + PageAlertMeta: model.PromAlertMeta{Disable: true}, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, + }, + }}, + }, + expErr: true, + }, + "Having SLOs it should generate Prometheus recording and alert rules.": { mocks: func(mspg *generatemock.SLOPluginGetter) { mspg.On("GetSLOPlugin", mock.Anything, "test-plugin1").Once().Return(&pluginengineslo.Plugin{ diff --git a/internal/plugin/slo/core/validate_v1/plugin.go b/internal/plugin/slo/core/validate_v1/plugin.go new file mode 100644 index 00000000..226a8cab --- /dev/null +++ b/internal/plugin/slo/core/validate_v1/plugin.go @@ -0,0 +1,34 @@ +package plugin + +import ( + "context" + "encoding/json" + "fmt" + + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/core/validate/v1" +) + +func NewPlugin(_ json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return plugin{ + appUtils: appUtils, + }, nil +} + +type plugin struct { + appUtils pluginslov1.AppUtils +} + +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + // TODO(slok): Should we stop using validator libraries and just use our own simple validation logic created here? + err := request.SLO.Validate() + if err != nil { + return fmt.Errorf("invalid slo %q: %w", request.SLO.ID, err) + } + + return nil +} diff --git a/internal/plugin/slo/core/validate_v1/plugin_test.go b/internal/plugin/slo/core/validate_v1/plugin_test.go new file mode 100644 index 00000000..d6f2a442 --- /dev/null +++ b/internal/plugin/slo/core/validate_v1/plugin_test.go @@ -0,0 +1,134 @@ +package plugin_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + plugin "github.com/slok/sloth/internal/plugin/slo/core/validate_v1" + "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" +) + +func getGoodSLO() model.PromSLO { + return model.PromSLO{ + ID: "slo1-id", + Name: "test.slo-0_1", + Service: "test-svc", + TimeWindow: 30 * 24 * time.Hour, + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))`, + TotalQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp"}[{{ .window }}]))`, + }, + }, + Objective: 99.99, + Labels: map[string]string{ + "owner": "myteam", + "category": "test", + }, + PageAlertMeta: model.PromAlertMeta{ + Disable: false, + Name: "testAlert", + Labels: map[string]string{ + "tier": "1", + "severity": "slack", + "channel": "#a-myteam", + }, + Annotations: map[string]string{ + "message": "This is very important.", + "runbook": "http://whatever.com", + }, + }, + TicketAlertMeta: model.PromAlertMeta{ + Disable: false, + Name: "testAlert", + Labels: map[string]string{ + "tier": "1", + "severity": "slack", + "channel": "#a-not-so-important", + }, + Annotations: map[string]string{ + "message": "This is not very important.", + "runbook": "http://whatever.com", + }, + }, + } +} + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + config json.RawMessage + req pluginslov1.Request + expRes pluginslov1.Result + expErr bool + }{ + "An invalid SLO should fail.": { + req: pluginslov1.Request{ + SLO: model.PromSLO{ + ID: "test", + }, + }, + expErr: true, + }, + + "A correct SLO should not fail.": { + req: pluginslov1.Request{ + SLO: getGoodSLO(), + }, + expRes: pluginslov1.Result{}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: test.config}) + require.NoError(err) + + res := pluginslov1.Result{} + err = plugin.ProcessSLO(t.Context(), &test.req, &res) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, res) + } + }) + } +} + +func BenchmarkPluginYaegi(b *testing.B) { + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: []byte(`{}`)}) + if err != nil { + b.Fatal(err) + } + + slo := getGoodSLO() + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{SLO: slo}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPluginGo(b *testing.B) { + plugin, err := plugin.NewPlugin([]byte(`{}`), pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } + + slo := getGoodSLO() + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{SLO: slo}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index 416de9cd..1884e3c2 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -67,7 +67,7 @@ type PromSLOPluginMetadata struct { } type PromSLOGroup struct { - SLOs []PromSLO `validate:"required,dive"` + SLOs []PromSLO OriginalSource PromSLOGroupSource } @@ -80,7 +80,7 @@ type PromSLOGroupSource struct { } // Validate validates the SLO. -func (s PromSLOGroup) Validate() error { +func (s PromSLO) Validate() error { return modelSpecValidate.Struct(s) } @@ -96,7 +96,6 @@ var modelSpecValidate = func() *validator.Validate { mustRegisterValidation(v, "required_if_enabled", validateRequiredEnabledAlertName) mustRegisterValidation(v, "template_vars", validateTemplateVars) v.RegisterStructValidation(validateOneSLI, PromSLI{}) - v.RegisterStructValidation(validateSLOGroup, PromSLOGroup{}) v.RegisterStructValidation(validateSLIEvents, PromSLIEvents{}) return v }() @@ -268,29 +267,6 @@ func validateOneSLI(sl validator.StructLevel) { } } -// validateSLOGroup validates SLO IDs are not repeated. -func validateSLOGroup(sl validator.StructLevel) { - sloGroup, ok := sl.Current().Interface().(PromSLOGroup) - if !ok { - sl.ReportError(sloGroup, "", "SLOGroup", "not_slo_group", "") - return - } - - if len(sloGroup.SLOs) == 0 { - sl.ReportError(sloGroup, "", "", "slos_required", "") - } - - // Check SLO IDs not repeated. - sloIDs := map[string]struct{}{} - for _, slo := range sloGroup.SLOs { - _, ok := sloIDs[slo.ID] - if ok { - sl.ReportError(slo.ID, slo.ID, "", "slo_repeated", "") - } - sloIDs[slo.ID] = struct{}{} - } -} - // PromSLORules are the prometheus rules required by an SLO. type PromSLORules struct { SLIErrorRecRules PromRuleGroup diff --git a/pkg/common/model/slo_prometheus_test.go b/pkg/common/model/slo_prometheus_test.go index 2529e364..d711077b 100644 --- a/pkg/common/model/slo_prometheus_test.go +++ b/pkg/common/model/slo_prometheus_test.go @@ -9,51 +9,47 @@ import ( "github.com/slok/sloth/pkg/common/model" ) -func getGoodSLOGroup() model.PromSLOGroup { - return model.PromSLOGroup{ - SLOs: []model.PromSLO{ - { - ID: "slo1-id", - Name: "test.slo-0_1", - Service: "test-svc", - TimeWindow: 30 * 24 * time.Hour, - SLI: model.PromSLI{ - Events: &model.PromSLIEvents{ - ErrorQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))`, - TotalQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp"}[{{ .window }}]))`, - }, - }, - Objective: 99.99, - Labels: map[string]string{ - "owner": "myteam", - "category": "test", - }, - PageAlertMeta: model.PromAlertMeta{ - Disable: false, - Name: "testAlert", - Labels: map[string]string{ - "tier": "1", - "severity": "slack", - "channel": "#a-myteam", - }, - Annotations: map[string]string{ - "message": "This is very important.", - "runbook": "http://whatever.com", - }, - }, - TicketAlertMeta: model.PromAlertMeta{ - Disable: false, - Name: "testAlert", - Labels: map[string]string{ - "tier": "1", - "severity": "slack", - "channel": "#a-not-so-important", - }, - Annotations: map[string]string{ - "message": "This is not very important.", - "runbook": "http://whatever.com", - }, - }, +func getGoodSLO() model.PromSLO { + return model.PromSLO{ + ID: "slo1-id", + Name: "test.slo-0_1", + Service: "test-svc", + TimeWindow: 30 * 24 * time.Hour, + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))`, + TotalQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp"}[{{ .window }}]))`, + }, + }, + Objective: 99.99, + Labels: map[string]string{ + "owner": "myteam", + "category": "test", + }, + PageAlertMeta: model.PromAlertMeta{ + Disable: false, + Name: "testAlert", + Labels: map[string]string{ + "tier": "1", + "severity": "slack", + "channel": "#a-myteam", + }, + Annotations: map[string]string{ + "message": "This is very important.", + "runbook": "http://whatever.com", + }, + }, + TicketAlertMeta: model.PromAlertMeta{ + Disable: false, + Name: "testAlert", + Labels: map[string]string{ + "tier": "1", + "severity": "slack", + "channel": "#a-not-so-important", + }, + Annotations: map[string]string{ + "message": "This is not very important.", + "runbook": "http://whatever.com", }, }, } @@ -61,389 +57,371 @@ func getGoodSLOGroup() model.PromSLOGroup { func TestModelValidationSpec(t *testing.T) { tests := map[string]struct { - slo func() model.PromSLOGroup + slo func() model.PromSLO expErrMessage string }{ "Correct SLO should not fail.": { - slo: func() model.PromSLOGroup { - return getGoodSLOGroup() + slo: func() model.PromSLO { + return getGoodSLO() }, }, - "SLOs must exist.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs = []model.PromSLO{} - return s - }, - expErrMessage: "Key: 'PromSLOGroup.' Error:Field validation for '' failed on the 'slos_required' tag", - }, - - "SLOs can't be repeated.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs = append(s.SLOs, s.SLOs[0]) - return s - }, - expErrMessage: "Key: 'PromSLOGroup.slo1-id' Error:Field validation for 'slo1-id' failed on the 'slo_repeated' tag", - }, - "SLO ID is required.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].ID = "" + slo: func() model.PromSLO { + s := getGoodSLO() + s.ID = "" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLO.ID' Error:Field validation for 'ID' failed on the 'required' tag", }, "SLO ID must be alphanumeric, `.`, '_', and '-'.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].ID = "this-is-{a-test" + slo: func() model.PromSLO { + s := getGoodSLO() + s.ID = "this-is-{a-test" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLO.ID' Error:Field validation for 'ID' failed on the 'name' tag", }, "SLO ID must start with aphanumeric.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].ID = "_" + s.SLOs[0].ID + slo: func() model.PromSLO { + s := getGoodSLO() + s.ID = "_" + s.ID return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLO.ID' Error:Field validation for 'ID' failed on the 'name' tag", }, "SLO ID must end with aphanumeric.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].ID = s.SLOs[0].ID + "_" + slo: func() model.PromSLO { + s := getGoodSLO() + s.ID = s.ID + "_" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].ID' Error:Field validation for 'ID' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLO.ID' Error:Field validation for 'ID' failed on the 'name' tag", }, "SLO Name is required.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Name = "" + slo: func() model.PromSLO { + s := getGoodSLO() + s.Name = "" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLO.Name' Error:Field validation for 'Name' failed on the 'required' tag", }, "SLO Name must be alphanumeric, `.`, '_', and '-'.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Name = "this-is-{a-test" + slo: func() model.PromSLO { + s := getGoodSLO() + s.Name = "this-is-{a-test" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLO.Name' Error:Field validation for 'Name' failed on the 'name' tag", }, "SLO Name must start with aphanumeric.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Name = "_" + s.SLOs[0].Name + slo: func() model.PromSLO { + s := getGoodSLO() + s.Name = "_" + s.Name return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLO.Name' Error:Field validation for 'Name' failed on the 'name' tag", }, "SLO Name must end with aphanumeric.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Name = s.SLOs[0].Name + "_" + slo: func() model.PromSLO { + s := getGoodSLO() + s.Name = s.Name + "_" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Name' Error:Field validation for 'Name' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLO.Name' Error:Field validation for 'Name' failed on the 'name' tag", }, "SLO Service is required.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Service = "" + slo: func() model.PromSLO { + s := getGoodSLO() + s.Service = "" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLO.Service' Error:Field validation for 'Service' failed on the 'required' tag", }, "SLO Service must be alphanumeric, `.`, '_', and '-'.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Service = "this-is-{a-test" + slo: func() model.PromSLO { + s := getGoodSLO() + s.Service = "this-is-{a-test" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLO.Service' Error:Field validation for 'Service' failed on the 'name' tag", }, "SLO Service must start with aphanumeric.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Service = "_" + s.SLOs[0].Service + slo: func() model.PromSLO { + s := getGoodSLO() + s.Service = "_" + s.Service return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLO.Service' Error:Field validation for 'Service' failed on the 'name' tag", }, "SLO Service must end with aphanumeric.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Service = s.SLOs[0].Service + "_" + slo: func() model.PromSLO { + s := getGoodSLO() + s.Service = s.Service + "_" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Service' Error:Field validation for 'Service' failed on the 'name' tag", + expErrMessage: "Key: 'PromSLO.Service' Error:Field validation for 'Service' failed on the 'name' tag", }, "SLO without SLI type should fail.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].SLI = model.PromSLI{} + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI = model.PromSLI{} return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.' Error:Field validation for '' failed on the 'sli_type_required' tag", + expErrMessage: "Key: 'PromSLO.SLI.' Error:Field validation for '' failed on the 'sli_type_required' tag", }, "SLO with more than one SLI type should fail.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].SLI.Raw = &model.PromSLIRaw{ + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI.Raw = &model.PromSLIRaw{ ErrorRatioQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))`, } return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.' Error:Field validation for '' failed on the 'one_sli_type' tag", + expErrMessage: "Key: 'PromSLO.SLI.' Error:Field validation for '' failed on the 'one_sli_type' tag", }, "SLO SLI event queries must be different.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].SLI.Events.TotalQuery = `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))` - s.SLOs[0].SLI.Events.ErrorQuery = `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))` + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI.Events.TotalQuery = `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))` + s.SLI.Events.ErrorQuery = `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))` return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.Events.' Error:Field validation for '' failed on the 'sli_events_queries_different' tag", + expErrMessage: "Key: 'PromSLO.SLI.Events.' Error:Field validation for '' failed on the 'sli_events_queries_different' tag", }, "SLO SLI error query should be valid Prometheus expr.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].SLI.Events.ErrorQuery = "sum(rate(grpc_server_handled_requests_count{[1m]))" + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI.Events.ErrorQuery = "sum(rate(grpc_server_handled_requests_count{[1m]))" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.Events.ErrorQuery' Error:Field validation for 'ErrorQuery' failed on the 'prom_expr' tag", + expErrMessage: "Key: 'PromSLO.SLI.Events.ErrorQuery' Error:Field validation for 'ErrorQuery' failed on the 'prom_expr' tag", }, "SLO SLI error query should have required template vars.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].SLI.Events.ErrorQuery = "sum(rate(grpc_server_handled_requests_count[1m]))" + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI.Events.ErrorQuery = "sum(rate(grpc_server_handled_requests_count[1m]))" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.Events.ErrorQuery' Error:Field validation for 'ErrorQuery' failed on the 'template_vars' tag", + expErrMessage: "Key: 'PromSLO.SLI.Events.ErrorQuery' Error:Field validation for 'ErrorQuery' failed on the 'template_vars' tag", }, "SLO SLI total query should be valid Prometheus expr.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].SLI.Events.TotalQuery = "sum(rate(grpc_server_handled_requests_count{[1m]))" + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI.Events.TotalQuery = "sum(rate(grpc_server_handled_requests_count{[1m]))" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.Events.TotalQuery' Error:Field validation for 'TotalQuery' failed on the 'prom_expr' tag", + expErrMessage: "Key: 'PromSLO.SLI.Events.TotalQuery' Error:Field validation for 'TotalQuery' failed on the 'prom_expr' tag", }, "SLO SLI total query should have required template vars.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].SLI.Events.TotalQuery = "sum(rate(grpc_server_handled_requests_count[1m]))" + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI.Events.TotalQuery = "sum(rate(grpc_server_handled_requests_count[1m]))" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].SLI.Events.TotalQuery' Error:Field validation for 'TotalQuery' failed on the 'template_vars' tag", + expErrMessage: "Key: 'PromSLO.SLI.Events.TotalQuery' Error:Field validation for 'TotalQuery' failed on the 'template_vars' tag", }, "SLO Objective shouldn't be less than 0.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Objective = -1 + slo: func() model.PromSLO { + s := getGoodSLO() + s.Objective = -1 return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Objective' Error:Field validation for 'Objective' failed on the 'gt' tag", + expErrMessage: "Key: 'PromSLO.Objective' Error:Field validation for 'Objective' failed on the 'gt' tag", }, "SLO Objective shouldn't be 0.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Objective = 0 + slo: func() model.PromSLO { + s := getGoodSLO() + s.Objective = 0 return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Objective' Error:Field validation for 'Objective' failed on the 'gt' tag", + expErrMessage: "Key: 'PromSLO.Objective' Error:Field validation for 'Objective' failed on the 'gt' tag", }, "SLO Objective shouldn't be greater than 100.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Objective = 100.0001 + slo: func() model.PromSLO { + s := getGoodSLO() + s.Objective = 100.0001 return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Objective' Error:Field validation for 'Objective' failed on the 'lte' tag", + expErrMessage: "Key: 'PromSLO.Objective' Error:Field validation for 'Objective' failed on the 'lte' tag", }, "SLO Labels should be valid prometheus keys.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" + slo: func() model.PromSLO { + s := getGoodSLO() + s.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", + expErrMessage: "Key: 'PromSLO.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", }, "SLO Labels should have prometheus values.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Labels["something"] = "" + slo: func() model.PromSLO { + s := getGoodSLO() + s.Labels["something"] = "" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLO.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", }, "SLO Labels should be valid prometheus values.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].Labels["something"] = "\xc3\x28" + slo: func() model.PromSLO { + s := getGoodSLO() + s.Labels["something"] = "\xc3\x28" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", + expErrMessage: "Key: 'PromSLO.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", }, "SLO page alert name is required.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].PageAlertMeta.Name = "" + slo: func() model.PromSLO { + s := getGoodSLO() + s.PageAlertMeta.Name = "" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Name' Error:Field validation for 'Name' failed on the 'required_if_enabled' tag", + expErrMessage: "Key: 'PromSLO.PageAlertMeta.Name' Error:Field validation for 'Name' failed on the 'required_if_enabled' tag", }, "SLO page alert fields are not required if disabled .": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].PageAlertMeta.Name = "" - s.SLOs[0].PageAlertMeta.Disable = true - s.SLOs[0].PageAlertMeta.Labels = map[string]string{} - s.SLOs[0].PageAlertMeta.Annotations = map[string]string{} + slo: func() model.PromSLO { + s := getGoodSLO() + s.PageAlertMeta.Name = "" + s.PageAlertMeta.Disable = true + s.PageAlertMeta.Labels = map[string]string{} + s.PageAlertMeta.Annotations = map[string]string{} return s }, }, "SLO warning alert name is required.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].TicketAlertMeta.Name = "" + slo: func() model.PromSLO { + s := getGoodSLO() + s.TicketAlertMeta.Name = "" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Name' Error:Field validation for 'Name' failed on the 'required_if_enabled' tag", + expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Name' Error:Field validation for 'Name' failed on the 'required_if_enabled' tag", }, "SLO warning alert fields are not required if disabled .": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].TicketAlertMeta.Name = "" - s.SLOs[0].TicketAlertMeta.Disable = true - s.SLOs[0].TicketAlertMeta.Labels = map[string]string{} - s.SLOs[0].TicketAlertMeta.Annotations = map[string]string{} + slo: func() model.PromSLO { + s := getGoodSLO() + s.TicketAlertMeta.Name = "" + s.TicketAlertMeta.Disable = true + s.TicketAlertMeta.Labels = map[string]string{} + s.TicketAlertMeta.Annotations = map[string]string{} return s }, }, "SLO page alert labels should be valid prometheus keys.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].PageAlertMeta.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" + slo: func() model.PromSLO { + s := getGoodSLO() + s.PageAlertMeta.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", + expErrMessage: "Key: 'PromSLO.PageAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", }, "SLO page alert labels should have prometheus values.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].PageAlertMeta.Labels["something"] = "" + slo: func() model.PromSLO { + s := getGoodSLO() + s.PageAlertMeta.Labels["something"] = "" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLO.PageAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", }, "SLO page alert labels should be valid prometheus values.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].PageAlertMeta.Labels["something"] = "\xc3\x28" + slo: func() model.PromSLO { + s := getGoodSLO() + s.PageAlertMeta.Labels["something"] = "\xc3\x28" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", + expErrMessage: "Key: 'PromSLO.PageAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", }, "SLO page alert annotations should be valid prometheus keys.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].PageAlertMeta.Annotations["\xF0\x8F\xBF\xBF"] = "label key is wrong" + slo: func() model.PromSLO { + s := getGoodSLO() + s.PageAlertMeta.Annotations["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", + expErrMessage: "Key: 'PromSLO.PageAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", }, "SLO page alert annotations should have prometheus values.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].PageAlertMeta.Annotations["something"] = "" + slo: func() model.PromSLO { + s := getGoodSLO() + s.PageAlertMeta.Annotations["something"] = "" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].PageAlertMeta.Annotations[something]' Error:Field validation for 'Annotations[something]' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLO.PageAlertMeta.Annotations[something]' Error:Field validation for 'Annotations[something]' failed on the 'required' tag", }, "SLO warning alert labels should be valid prometheus keys.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].TicketAlertMeta.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" + slo: func() model.PromSLO { + s := getGoodSLO() + s.TicketAlertMeta.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", + expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", }, "SLO warning alert labels should have prometheus values.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].TicketAlertMeta.Labels["something"] = "" + slo: func() model.PromSLO { + s := getGoodSLO() + s.TicketAlertMeta.Labels["something"] = "" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", }, "SLO warning alert labels should be valid prometheus values.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].TicketAlertMeta.Labels["something"] = "\xc3\x28" + slo: func() model.PromSLO { + s := getGoodSLO() + s.TicketAlertMeta.Labels["something"] = "\xc3\x28" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", + expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", }, "SLO warning alert annotations should be valid prometheus keys.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].TicketAlertMeta.Annotations["\xF0\x8F\xBF\xBF"] = "label key is wrong" + slo: func() model.PromSLO { + s := getGoodSLO() + s.TicketAlertMeta.Annotations["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", + expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", }, "SLO warning alert annotations should have prometheus values.": { - slo: func() model.PromSLOGroup { - s := getGoodSLOGroup() - s.SLOs[0].TicketAlertMeta.Annotations["something"] = "" + slo: func() model.PromSLO { + s := getGoodSLO() + s.TicketAlertMeta.Annotations["something"] = "" return s }, - expErrMessage: "Key: 'PromSLOGroup.SLOs[0].TicketAlertMeta.Annotations[something]' Error:Field validation for 'Annotations[something]' failed on the 'required' tag", + expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Annotations[something]' Error:Field validation for 'Annotations[something]' failed on the 'required' tag", }, } From 645eef4ad57bfaa0e50b9d8e2c98f198186188ab Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Fri, 11 Apr 2025 07:37:49 +0200 Subject: [PATCH 045/173] Add the original SLO group source information to the plugin request Signed-off-by: Xabier Larrakoetxea --- internal/app/generate/generate.go | 5 +++-- internal/app/generate/process.go | 2 ++ pkg/prometheus/plugin/slo/v1/v1.go | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/app/generate/generate.go b/internal/app/generate/generate.go index 273d1a48..e9dff277 100644 --- a/internal/app/generate/generate.go +++ b/internal/app/generate/generate.go @@ -175,7 +175,7 @@ func (s Service) Generate(ctx context.Context, r Request) (*Response, error) { slo.Labels = utilsdata.MergeLabels(slo.Labels, r.ExtraLabels) // Generate SLO result. - result, err := s.generateSLO(ctx, r.Info, slo) + result, err := s.generateSLO(ctx, r.Info, r.SLOGroup, slo) if err != nil { return nil, fmt.Errorf("could not generate %q slo: %w", slo.ID, err) } @@ -188,7 +188,7 @@ func (s Service) Generate(ctx context.Context, r Request) (*Response, error) { }, nil } -func (s Service) generateSLO(ctx context.Context, info model.Info, slo model.PromSLO) (*model.PromSLORules, error) { +func (s Service) generateSLO(ctx context.Context, info model.Info, sloGroup model.PromSLOGroup, slo model.PromSLO) (*model.PromSLORules, error) { logger := s.logger.WithCtxValues(ctx).WithValues(log.Kv{"slo": slo.ID}) // Generate the MWMB alerts. @@ -242,6 +242,7 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, slo model.Pro Info: info, MWMBAlertGroup: *as, SLO: slo, + SLOGroup: sloGroup, } res := &SLOProcessorResult{} for _, p := range sloProcessors { diff --git a/internal/app/generate/process.go b/internal/app/generate/process.go index 0dba86ea..33de3e7e 100644 --- a/internal/app/generate/process.go +++ b/internal/app/generate/process.go @@ -13,6 +13,7 @@ import ( type SLOProcessorRequest struct { Info model.Info SLO model.PromSLO + SLOGroup model.PromSLOGroup MWMBAlertGroup model.MWMBAlertGroup } type SLOProcessorResult struct { @@ -51,6 +52,7 @@ func NewSLOProcessorFromSLOPluginV1(pluginFactory pluginslov1.PluginFactory, log Info: req.Info, SLO: req.SLO, MWMBAlertGroup: req.MWMBAlertGroup, + OriginalSource: req.SLOGroup.OriginalSource, } rs := &pluginslov1.Result{ SLORules: res.SLORules, diff --git a/pkg/prometheus/plugin/slo/v1/v1.go b/pkg/prometheus/plugin/slo/v1/v1.go index 2881f81b..9d1118a2 100644 --- a/pkg/prometheus/plugin/slo/v1/v1.go +++ b/pkg/prometheus/plugin/slo/v1/v1.go @@ -29,6 +29,10 @@ type AppUtils struct { type Request struct { // Info about the application and execution, normally used as metadata. Info model.Info + // OriginalSource is the original specification of the SLO came from, this is informative data that + // can be used to make decision on plugins, it should be used only as RO. + // The information used on the generation is the SLO model itself not this one. + OriginalSource model.PromSLOGroupSource // The SLO to process and generate the final Prom rules. SLO model.PromSLO // The SLO MWMBAlertGroup selected. From 5db99257baaab6ebdb814b93f4cff21e38d5bf32 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Fri, 11 Apr 2025 09:19:55 -0400 Subject: [PATCH 046/173] fix: update go version go.mod solves issues with go downloading https://stackoverflow.com/q/78519711 ``` go: downloading go1.24 (linux/amd64) go: download go1.24 for linux/amd64: toolchain not available ``` --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 55ecfa6f..0f0c380b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/slok/sloth -go 1.24 +go 1.24.0 require ( github.com/OpenSLO/oslo v0.12.0 From baef4be23a21c44419fdd62b803d9340f72db3da Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Fri, 11 Apr 2025 18:59:00 +0200 Subject: [PATCH 047/173] Aggregate SLO and SLI plugin file loader into a single repo that discovers both and change flags to improve usage Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 1 + cmd/sloth/commands/generate.go | 23 +- cmd/sloth/commands/helpers.go | 22 +- cmd/sloth/commands/k8scontroller.go | 23 +- cmd/sloth/commands/validate.go | 19 +- .../helm/sloth/templates/deployment.yaml | 2 +- .../testdata/output/deployment_custom.yaml | 2 +- .../testdata/output/deployment_default.yaml | 2 +- .../raw/sloth-with-common-plugins.yaml | 2 +- internal/storage/fs/plugin.go | 170 ++++++++ internal/storage/fs/plugin_test.go | 364 ++++++++++++++++++ internal/storage/fs/sli_plugin.go | 197 ---------- internal/storage/fs/sli_plugin_test.go | 152 -------- internal/storage/fs/slo_plugin.go | 124 ------ internal/storage/fs/slo_plugin_test.go | 178 --------- .../k8scontroller/k8scontroller_test.go | 2 +- test/integration/prometheus/helpers.go | 6 +- .../sli}/plugin1/plugin.go | 0 .../slo}/plugin1/plugin.go | 0 19 files changed, 566 insertions(+), 723 deletions(-) create mode 100644 internal/storage/fs/plugin.go create mode 100644 internal/storage/fs/plugin_test.go delete mode 100644 internal/storage/fs/sli_plugin.go delete mode 100644 internal/storage/fs/sli_plugin_test.go delete mode 100644 internal/storage/fs/slo_plugin.go delete mode 100644 internal/storage/fs/slo_plugin_test.go rename test/integration/prometheus/{sli_plugins => plugins/sli}/plugin1/plugin.go (100%) rename test/integration/prometheus/{slo_plugins => plugins/slo}/plugin1/plugin.go (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c009235..0ff9d396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Split image registry and repository in Helm chart - (BREAKING) Internally Sloth (not k8s) prometheusServiceLevel uses k8s `k8s.io/apimachinery/pkg/util/yaml` lib for unmarshaling YAML instead of `gopkg.in/yaml.v2`. - Core SLO validation and SLO rules generation migrated to SLO plugins. +- (BREAKING) `--sli-plugins-path`, `--slo-plugins-path`, `-m` args and it's env vars `SLOTH_SLI_PLUGINS_PATH`and `SLOTH_SLO_PLUGINS_PATH` have been removed in favor or `--plugins-path`, `-p` and it's env var `SLOTH_PLUGINS_PATH` that discovers and loads SLI and SLO plugins with a single flag. ### Added diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 5bf337a6..1a3aa750 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -41,8 +41,7 @@ type generateCommand struct { disableAlerts bool disableOptimizedRules bool extraLabels map[string]string - sliPluginsPaths []string - sloPluginsPaths []string + pluginsPaths []string sloPeriodWindowsPath string sloPeriod string } @@ -59,8 +58,7 @@ func NewGenerateCommand(app *kingpin.Application) Command { cmd.Flag("extra-labels", "Extra labels that will be added to all the generated Prometheus rules ('key=value' form, can be repeated).").Short('l').StringMapVar(&c.extraLabels) cmd.Flag("disable-recordings", "Disables recording rules generation.").BoolVar(&c.disableRecordings) cmd.Flag("disable-alerts", "Disables alert rules generation.").BoolVar(&c.disableAlerts) - cmd.Flag("sli-plugins-path", "The path to SLI plugins (can be repeated), if not set it disable plugins support.").Short('p').StringsVar(&c.sliPluginsPaths) - cmd.Flag("slo-plugins-path", "The path to SLO plugins a.k.a SLO generation middlewares (can be repeated).").Short('m').StringsVar(&c.sloPluginsPaths) + cmd.Flag("plugins-path", "The path to SLI and SLO plugins (can be repeated).").Short('p').StringsVar(&c.pluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) cmd.Flag("disable-optimized-rules", "If enabled it will disable optimized generated rules.").BoolVar(&c.disableOptimizedRules) @@ -112,13 +110,8 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { "out": g.slosOut, }) - // Load plugins - pluginSLIRepo, err := createSLIPluginLoader(ctx, logger, g.sliPluginsPaths) - if err != nil { - return err - } - - pluginSLORepo, err := createSLOPluginLoader(ctx, logger, g.sloPluginsPaths) + // Load plugins. + pluginsRepo, err := createPluginLoader(ctx, logger, g.pluginsPaths) if err != nil { return err } @@ -143,8 +136,8 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { } // Create Spec loaders. - promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginSLIRepo, sloPeriod) - kubeYAMLLoader := storageio.NewK8sSlothPrometheusYAMLSpecLoader(pluginSLIRepo, sloPeriod) + promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginsRepo, sloPeriod) + kubeYAMLLoader := storageio.NewK8sSlothPrometheusYAMLSpecLoader(pluginsRepo, sloPeriod) openSLOYAMLLoader := storageio.NewOpenSLOYAMLSpecLoader(sloPeriod) // Get SLO targets. @@ -257,7 +250,7 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { disableAlerts: g.disableAlerts, disableOptimizedRules: g.disableOptimizedRules, extraLabels: g.extraLabels, - sloPluginRepo: pluginSLORepo, + sloPluginRepo: pluginsRepo, } for _, genTarget := range genTargets { @@ -318,7 +311,7 @@ type generator struct { disableAlerts bool disableOptimizedRules bool extraLabels map[string]string - sloPluginRepo *storagefs.FileSLOPluginRepo + sloPluginRepo *storagefs.FilePluginRepo } // GeneratePrometheus generates the SLOs based on a raw regular Prometheus spec format input and outs a Prometheus raw yaml. diff --git a/cmd/sloth/commands/helpers.go b/cmd/sloth/commands/helpers.go index c0b4c49e..72c05b19 100644 --- a/cmd/sloth/commands/helpers.go +++ b/cmd/sloth/commands/helpers.go @@ -42,21 +42,7 @@ func splitYAML(data []byte) []string { return nonEmptyData } -func createSLIPluginLoader(ctx context.Context, logger log.Logger, paths []string) (*storagefs.FileSLIPluginRepo, error) { - config := storagefs.FileSLIPluginRepoConfig{ - Paths: paths, - PluginLoader: pluginenginesli.PluginLoader, - Logger: logger, - } - sliPluginRepo, err := storagefs.NewFileSLIPluginRepo(config) - if err != nil { - return nil, fmt.Errorf("could not create file SLI plugin repository: %w", err) - } - - return sliPluginRepo, nil -} - -func createSLOPluginLoader(ctx context.Context, logger log.Logger, paths []string) (*storagefs.FileSLOPluginRepo, error) { +func createPluginLoader(ctx context.Context, logger log.Logger, paths []string) (*storagefs.FilePluginRepo, error) { fss := []fs.FS{ plugin.EmbeddedDefaultSLOPlugins, } @@ -64,12 +50,12 @@ func createSLOPluginLoader(ctx context.Context, logger log.Logger, paths []strin fss = append(fss, os.DirFS(p)) } - sliPluginRepo, err := storagefs.NewFileSLOPluginRepo(logger, pluginengineslo.PluginLoader, fss...) + pluginsRepo, err := storagefs.NewFilePluginRepo(logger, pluginenginesli.PluginLoader, pluginengineslo.PluginLoader, fss...) if err != nil { - return nil, fmt.Errorf("could not create file SLO plugin repository: %w", err) + return nil, fmt.Errorf("could not create file SLO and SLI plugins repository: %w", err) } - return sliPluginRepo, nil + return pluginsRepo, nil } func discoverSLOManifests(logger log.Logger, exclude, include *regexp.Regexp, path string) ([]string, error) { diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index e57a834c..087d37ca 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -69,8 +69,7 @@ type kubeControllerCommand struct { hotReloadPath string hotReloadAddr string metricsListenAddr string - sliPluginsPaths []string - sloPluginsPaths []string + pluginsPaths []string sloPeriodWindowsPath string sloPeriod string disableOptimizedRules bool @@ -97,8 +96,7 @@ func NewKubeControllerCommand(app *kingpin.Application) Command { cmd.Flag("hot-reload-addr", "The listen address for hot-reloading components that allow it.").Default(":8082").StringVar(&c.hotReloadAddr) cmd.Flag("hot-reload-path", "The webhook path for hot-reloading components that allow it.").Default("/-/reload").StringVar(&c.hotReloadPath) cmd.Flag("extra-labels", "Extra labels that will be added to all the generated Prometheus rules ('key=value' form, can be repeated).").Short('l').StringMapVar(&c.extraLabels) - cmd.Flag("sli-plugins-path", "The path to SLI plugins (can be repeated), if not set it disable plugins support.").Short('p').StringsVar(&c.sliPluginsPaths) - cmd.Flag("slo-plugins-path", "The path to SLO plugins a.k.a SLO generation middlewares (can be repeated).").Short('m').StringsVar(&c.sloPluginsPaths) + cmd.Flag("plugins-path", "The path to SLI and SLO plugins (can be repeated).").Short('p').StringsVar(&c.pluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) cmd.Flag("disable-optimized-rules", "If enabled it will disable optimized generated rules.").BoolVar(&c.disableOptimizedRules) @@ -118,12 +116,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error sloPeriod := time.Duration(sp) // Plugins. - pluginSLIRepo, err := createSLIPluginLoader(ctx, logger, k.sliPluginsPaths) - if err != nil { - return err - } - - pluginSLORepo, err := createSLOPluginLoader(ctx, logger, k.sloPluginsPaths) + pluginsRepo, err := createPluginLoader(ctx, logger, k.pluginsPaths) if err != nil { return err } @@ -169,11 +162,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error { // Set SLI plugin repository reloader. reloadManager.Add(1000, reload.ReloaderFunc(func(ctx context.Context, id string) error { - return pluginSLIRepo.Reload(ctx) - })) - - reloadManager.Add(1000, reload.ReloaderFunc(func(ctx context.Context, id string) error { - return pluginSLORepo.Reload(ctx) + return pluginsRepo.Reload(ctx) })) ctx, cancel := context.WithCancel(ctx) @@ -356,7 +345,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error MetadataRulesGenSLOPlugin: metaRuleGen, AlertRulesGenSLOPlugin: alertRuleGen, ValidateSLOPlugin: validatePlugin, - SLOPluginGetter: pluginSLORepo, + SLOPluginGetter: pluginsRepo, Logger: generatorLogger{Logger: logger}, }) if err != nil { @@ -366,7 +355,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error // Create handler. controllerConfig := kubecontroller.HandlerConfig{ Generator: generator, - SpecLoader: storageio.NewK8sSlothPrometheusCRSpecLoader(pluginSLIRepo, sloPeriod), + SpecLoader: storageio.NewK8sSlothPrometheusCRSpecLoader(pluginsRepo, sloPeriod), Repository: kuberepo, KubeStatusStorer: kuberepo, ExtraLabels: k.extraLabels, diff --git a/cmd/sloth/commands/validate.go b/cmd/sloth/commands/validate.go index f752e627..06628dcc 100644 --- a/cmd/sloth/commands/validate.go +++ b/cmd/sloth/commands/validate.go @@ -22,8 +22,7 @@ type validateCommand struct { slosExcludeRegex string slosIncludeRegex string extraLabels map[string]string - sliPluginsPaths []string - sloPluginsPaths []string + pluginsPaths []string sloPeriodWindowsPath string sloPeriod string } @@ -36,8 +35,7 @@ func NewValidateCommand(app *kingpin.Application) Command { cmd.Flag("fs-exclude", "Filter regex to ignore matched discovered SLO file paths.").Short('e').StringVar(&c.slosExcludeRegex) cmd.Flag("fs-include", "Filter regex to include matched discovered SLO file paths, everything else will be ignored. Exclude has preference.").Short('n').StringVar(&c.slosIncludeRegex) cmd.Flag("extra-labels", "Extra labels that will be added to all the generated Prometheus rules ('key=value' form, can be repeated).").Short('l').StringMapVar(&c.extraLabels) - cmd.Flag("sli-plugins-path", "The path to SLI plugins (can be repeated), if not set it disable plugins support.").Short('p').StringsVar(&c.sliPluginsPaths) - cmd.Flag("slo-plugins-path", "The path to SLO plugins a.k.a SLO generation middlewares (can be repeated).").Short('m').StringsVar(&c.sloPluginsPaths) + cmd.Flag("plugins-path", "The path to SLI and SLO plugins (can be repeated).").Short('p').StringsVar(&c.pluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) @@ -83,12 +81,7 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { } // Load plugins. - pluginSLIRepo, err := createSLIPluginLoader(ctx, logger, v.sliPluginsPaths) - if err != nil { - return err - } - - pluginSLORepo, err := createSLOPluginLoader(ctx, logger, v.sloPluginsPaths) + pluginsRepo, err := createPluginLoader(ctx, logger, v.pluginsPaths) if err != nil { return err } @@ -113,8 +106,8 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { } // Create Spec loaders. - promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginSLIRepo, sloPeriod) - kubeYAMLLoader := storageio.NewK8sSlothPrometheusYAMLSpecLoader(pluginSLIRepo, sloPeriod) + promYAMLLoader := storageio.NewSlothPrometheusYAMLSpecLoader(pluginsRepo, sloPeriod) + kubeYAMLLoader := storageio.NewK8sSlothPrometheusYAMLSpecLoader(pluginsRepo, sloPeriod) openSLOYAMLLoader := storageio.NewOpenSLOYAMLSpecLoader(sloPeriod) // For every file load the data and start the validation process: @@ -134,7 +127,7 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { logger: log.Noop, windowsRepo: windowsRepo, extraLabels: v.extraLabels, - sloPluginRepo: pluginSLORepo, + sloPluginRepo: pluginsRepo, } // Prepare file validation result and start validation result for every SLO in the file. diff --git a/deploy/kubernetes/helm/sloth/templates/deployment.yaml b/deploy/kubernetes/helm/sloth/templates/deployment.yaml index 3f6ac107..f856e092 100644 --- a/deploy/kubernetes/helm/sloth/templates/deployment.yaml +++ b/deploy/kubernetes/helm/sloth/templates/deployment.yaml @@ -52,7 +52,7 @@ spec: - --extra-labels={{ $key }}={{ $val }} {{- end}} {{- if .Values.commonPlugins.enabled }} - - --sli-plugins-path=/plugins + - --plugins-path=/plugins {{- end }} {{- with .Values.sloth.defaultSloPeriod }} - --default-slo-period={{ . }} diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml index 538270f2..ad3832ca 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml @@ -48,7 +48,7 @@ spec: - --label-selector=x=y,z!=y - --extra-labels=k1=v1 - --extra-labels=k2=v2 - - --sli-plugins-path=/plugins + - --plugins-path=/plugins - --disable-optimized-rules - --logger=default ports: diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml index 40805140..65091427 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml @@ -35,7 +35,7 @@ spec: image: ghcr.io/slok/sloth:v0.12.0 args: - kubernetes-controller - - --sli-plugins-path=/plugins + - --plugins-path=/plugins - --logger=default ports: - containerPort: 8081 diff --git a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml index 3c56d2ce..22503772 100644 --- a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml +++ b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml @@ -88,7 +88,7 @@ spec: image: ghcr.io/slok/sloth:v0.12.0 args: - kubernetes-controller - - --sli-plugins-path=/plugins + - --plugins-path=/plugins - --logger=default ports: - containerPort: 8081 diff --git a/internal/storage/fs/plugin.go b/internal/storage/fs/plugin.go new file mode 100644 index 00000000..5c782f6c --- /dev/null +++ b/internal/storage/fs/plugin.go @@ -0,0 +1,170 @@ +package fs + +import ( + "context" + "fmt" + "io/fs" + "regexp" + "sync" + + "github.com/slok/sloth/internal/log" + pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" + pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" + commonerrors "github.com/slok/sloth/pkg/common/errors" +) + +type SLIPluginLoader interface { + LoadRawSLIPlugin(ctx context.Context, src string) (*pluginenginesli.SLIPlugin, error) +} + +//go:generate mockery --case underscore --output fsmock --outpkg fsmock --name SLIPluginLoader + +type SLOPluginLoader interface { + LoadRawPlugin(ctx context.Context, src string) (*pluginengineslo.Plugin, error) +} + +//go:generate mockery --case underscore --output fsmock --outpkg fsmock --name SLOPluginLoader + +type FilePluginRepo struct { + fss []fs.FS + sloPluginLoader SLOPluginLoader + sliPluginLoader SLIPluginLoader + sloPluginCache map[string]pluginengineslo.Plugin + sliPluginCache map[string]pluginenginesli.SLIPlugin + logger log.Logger + mu sync.RWMutex +} + +// NewFilePluginRepo returns a new FilePluginRepo that loads SLI and SLO plugins from the given file system. +func NewFilePluginRepo(logger log.Logger, sliPluginLoader SLIPluginLoader, sloPluginLoader SLOPluginLoader, fss ...fs.FS) (*FilePluginRepo, error) { + r := &FilePluginRepo{ + fss: fss, + sliPluginLoader: sliPluginLoader, + sloPluginLoader: sloPluginLoader, + sloPluginCache: map[string]pluginengineslo.Plugin{}, + sliPluginCache: map[string]pluginenginesli.SLIPlugin{}, + logger: logger, + } + + err := r.Reload(context.Background()) + if err != nil { + return nil, fmt.Errorf("could not load plugins: %w", err) + } + + return r, nil +} + +var pluginNameRegex = regexp.MustCompile("plugin.go$") + +func (r *FilePluginRepo) Reload(ctx context.Context) error { + sloPlugins, sliPlugins, err := r.loadPlugins(ctx, r.fss...) + if err != nil { + return fmt.Errorf("could not load plugins: %w", err) + } + + // Set loaded plugins. + r.mu.Lock() + r.sloPluginCache = sloPlugins + r.sliPluginCache = sliPlugins + r.mu.Unlock() + + r.logger.WithValues(log.Kv{"slo-plugins": len(sloPlugins), "sli-plugins": len(sliPlugins)}).Infof("Plugins loaded") + return nil +} + +func (r *FilePluginRepo) GetSLOPlugin(ctx context.Context, id string) (*pluginengineslo.Plugin, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + p, ok := r.sloPluginCache[id] + if !ok { + return nil, fmt.Errorf("plugin %q not found: %w", id, commonerrors.ErrNotFound) + } + + return &p, nil +} + +func (r *FilePluginRepo) ListSLOPlugins(ctx context.Context) (map[string]pluginengineslo.Plugin, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + return r.sloPluginCache, nil +} + +func (r *FilePluginRepo) GetSLIPlugin(ctx context.Context, id string) (*pluginenginesli.SLIPlugin, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + p, ok := r.sliPluginCache[id] + if !ok { + return nil, fmt.Errorf("plugin %q not found: %w", id, commonerrors.ErrNotFound) + } + + return &p, nil +} + +func (r *FilePluginRepo) ListSLIPlugins(ctx context.Context) (map[string]pluginenginesli.SLIPlugin, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + return r.sliPluginCache, nil +} + +func (r *FilePluginRepo) loadPlugins(ctx context.Context, fss ...fs.FS) (map[string]pluginengineslo.Plugin, map[string]pluginenginesli.SLIPlugin, error) { + sloPlugins := map[string]pluginengineslo.Plugin{} + sliPlugins := map[string]pluginenginesli.SLIPlugin{} + + for _, f := range fss { + err := fs.WalkDir(f, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + + if !pluginNameRegex.MatchString(path) { + return nil + } + + pluginDataBytes, err := fs.ReadFile(f, path) + if err != nil { + return fmt.Errorf("could not read %q plugin data: %w", path, err) + } + pluginData := string(pluginDataBytes) + + // Try SLI plugin if not, SLO plugin. + sliPlugin, sliErr := r.sliPluginLoader.LoadRawSLIPlugin(ctx, pluginData) + if sliErr == nil { + _, ok := sliPlugins[sliPlugin.ID] + if ok { + return fmt.Errorf("plugin %q already loaded", sliPlugin.ID) + } + sliPlugins[sliPlugin.ID] = *sliPlugin + r.logger.WithValues(log.Kv{"sli-plugin-id": sliPlugin.ID}).Debugf("SLI plugin discovered and loaded") + return nil + } + + // Try SLO plugin. + sloPlugin, sloErr := r.sloPluginLoader.LoadRawPlugin(ctx, pluginData) + if sloErr == nil { + _, ok := sloPlugins[sloPlugin.ID] + if ok { + return fmt.Errorf("plugin %q already loaded", sloPlugin.ID) + } + sloPlugins[sloPlugin.ID] = *sloPlugin + r.logger.WithValues(log.Kv{"slo-plugin-id": sloPlugin.ID}).Debugf("SLO plugin discovered and loaded") + return nil + } + + r.logger.Errorf("could not load %q as SLI or SLO plugin: (SLI plugin error: %s | SLO plugin error: %s)", path, sliErr, sloErr) + + return nil + }) + if err != nil { + return nil, nil, fmt.Errorf("could not walk dir: %w", err) + } + } + + return sloPlugins, sliPlugins, nil +} diff --git a/internal/storage/fs/plugin_test.go b/internal/storage/fs/plugin_test.go new file mode 100644 index 00000000..d8329fd7 --- /dev/null +++ b/internal/storage/fs/plugin_test.go @@ -0,0 +1,364 @@ +package fs_test + +import ( + "fmt" + "io/fs" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/slok/sloth/internal/log" + pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" + pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" + storagefs "github.com/slok/sloth/internal/storage/fs" + "github.com/slok/sloth/internal/storage/fs/fsmock" +) + +func TestFilePluginRepoListSLOPlugins(t *testing.T) { + tests := map[string]struct { + fss func() []fs.FS + mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) + expPlugins map[string]pluginengineslo.Plugin + expLoadErr bool + expErr bool + }{ + "Having no files, should return empty list of plugins.": { + fss: func() []fs.FS { return nil }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) {}, + expPlugins: map[string]pluginengineslo.Plugin{}, + }, + + "Having plugins in multiple FS and directories, should return all plugins.": { + fss: func() []fs.FS { + m1 := make(fstest.MapFS) + m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + m1["m1/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p3")} + + m2 := make(fstest.MapFS) + m2["m2/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p4")} + m2["m2/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p5")} + m2["m2/plugin-test.go"] = &fstest.MapFile{Data: []byte("p8")} // Ignored. + + m3 := make(fstest.MapFS) + m3["m3/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p6")} + m3["m3/plx/pl3/plugin.yaml"] = &fstest.MapFile{Data: []byte("p7")} // Ignored. + + return []fs.FS{m1, m2, m3} + }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(nil, fmt.Errorf("something")) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p3").Once().Return(nil, fmt.Errorf("something")) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p4").Once().Return(nil, fmt.Errorf("something")) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p5").Once().Return(nil, fmt.Errorf("something")) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p6").Once().Return(nil, fmt.Errorf("something")) + + mslopl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) + mslopl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(&pluginengineslo.Plugin{ID: "p2"}, nil) + mslopl.On("LoadRawPlugin", mock.Anything, "p3").Once().Return(&pluginengineslo.Plugin{ID: "p3"}, nil) + mslopl.On("LoadRawPlugin", mock.Anything, "p4").Once().Return(&pluginengineslo.Plugin{ID: "p4"}, nil) + mslopl.On("LoadRawPlugin", mock.Anything, "p5").Once().Return(&pluginengineslo.Plugin{ID: "p5"}, nil) + mslopl.On("LoadRawPlugin", mock.Anything, "p6").Once().Return(&pluginengineslo.Plugin{ID: "p6"}, nil) + }, + expPlugins: map[string]pluginengineslo.Plugin{ + "p1": {ID: "p1"}, + "p2": {ID: "p2"}, + "p3": {ID: "p3"}, + "p4": {ID: "p4"}, + "p5": {ID: "p5"}, + "p6": {ID: "p6"}, + }, + }, + + "Having a plugin loaded with the same ID multiple times should fail.": { + fss: func() []fs.FS { + m1 := make(fstest.MapFS) + m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + + return []fs.FS{m1} + }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(nil, fmt.Errorf("something")) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + + mslopl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) + mslopl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) + + }, + expLoadErr: true, + }, + + "Having an error while loading a plugin, should not fail but don't load the failed plugin.": { + fss: func() []fs.FS { + m1 := make(fstest.MapFS) + m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + + return []fs.FS{m1} + }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(nil, fmt.Errorf("something")) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + + mslopl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) + mslopl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + + }, + expPlugins: map[string]pluginengineslo.Plugin{ + "p1": {ID: "p1"}, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + mslopl := fsmock.NewSLOPluginLoader(t) + mslipl := fsmock.NewSLIPluginLoader(t) + test.mock(mslopl, mslipl) + + // Create repository and load plugins. + repo, err := storagefs.NewFilePluginRepo(log.Noop, mslipl, mslopl, test.fss()...) + if test.expLoadErr { + assert.Error(err) + return + } + assert.NoError(err) + + plugins, err := repo.ListSLOPlugins(t.Context()) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expPlugins, plugins) + } + }) + } +} + +func TestFilePluginRepoGetSLOPlugin(t *testing.T) { + tests := map[string]struct { + pluginID string + fss func() []fs.FS + mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) + expPlugin pluginengineslo.Plugin + expErr bool + }{ + "Having no files, should fail.": { + pluginID: "test", + fss: func() []fs.FS { return nil }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) {}, + expErr: true, + }, + + "Getting a correct plugin, should return the plugin.": { + pluginID: "p2", + fss: func() []fs.FS { + m := make(fstest.MapFS) + m["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + return []fs.FS{m} + }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(nil, fmt.Errorf("something")) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + + mslopl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) + mslopl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(&pluginengineslo.Plugin{ID: "p2"}, nil) + }, + expPlugin: pluginengineslo.Plugin{ID: "p2"}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + mslopl := fsmock.NewSLOPluginLoader(t) + mslipl := fsmock.NewSLIPluginLoader(t) + test.mock(mslopl, mslipl) + + // Create repository and load plugins. + repo, err := storagefs.NewFilePluginRepo(log.Noop, mslipl, mslopl, test.fss()...) + require.NoError(err) + + plugin, err := repo.GetSLOPlugin(t.Context(), test.pluginID) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expPlugin, *plugin) + } + }) + } +} + +func TestFilePluginRepoListSLIPlugins(t *testing.T) { + tests := map[string]struct { + fss func() []fs.FS + mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) + expPlugins map[string]pluginenginesli.SLIPlugin + expLoadErr bool + expErr bool + }{ + "Having no files, should return empty list of plugins.": { + fss: func() []fs.FS { return nil }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) {}, + expPlugins: map[string]pluginenginesli.SLIPlugin{}, + }, + + "Having plugins in multiple FS and directories, should return all plugins.": { + fss: func() []fs.FS { + m1 := make(fstest.MapFS) + m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + m1["m1/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p3")} + + m2 := make(fstest.MapFS) + m2["m2/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p4")} + m2["m2/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p5")} + m2["m2/plugin-test.go"] = &fstest.MapFile{Data: []byte("p8")} // Ignored. + + m3 := make(fstest.MapFS) + m3["m3/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p6")} + m3["m3/plx/pl3/plugin.yaml"] = &fstest.MapFile{Data: []byte("p7")} // Ignored. + + return []fs.FS{m1, m2, m3} + }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(&pluginenginesli.SLIPlugin{ID: "p2"}, nil) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p3").Once().Return(&pluginenginesli.SLIPlugin{ID: "p3"}, nil) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p4").Once().Return(&pluginenginesli.SLIPlugin{ID: "p4"}, nil) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p5").Once().Return(&pluginenginesli.SLIPlugin{ID: "p5"}, nil) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p6").Once().Return(&pluginenginesli.SLIPlugin{ID: "p6"}, nil) + }, + expPlugins: map[string]pluginenginesli.SLIPlugin{ + "p1": {ID: "p1"}, + "p2": {ID: "p2"}, + "p3": {ID: "p3"}, + "p4": {ID: "p4"}, + "p5": {ID: "p5"}, + "p6": {ID: "p6"}, + }, + }, + + "Having a plugin loaded with the same ID multiple times should fail.": { + fss: func() []fs.FS { + m1 := make(fstest.MapFS) + m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + + return []fs.FS{m1} + }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) + }, + expLoadErr: true, + }, + + "Having an error while loading a plugin, should not fail but don't load the failed plugin.": { + fss: func() []fs.FS { + m1 := make(fstest.MapFS) + m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + + return []fs.FS{m1} + }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + mslopl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + + }, + expPlugins: map[string]pluginenginesli.SLIPlugin{ + "p1": {ID: "p1"}, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + mslopl := fsmock.NewSLOPluginLoader(t) + mslipl := fsmock.NewSLIPluginLoader(t) + test.mock(mslopl, mslipl) + + // Create repository and load plugins. + repo, err := storagefs.NewFilePluginRepo(log.Noop, mslipl, mslopl, test.fss()...) + if test.expLoadErr { + assert.Error(err) + return + } + assert.NoError(err) + + plugins, err := repo.ListSLIPlugins(t.Context()) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expPlugins, plugins) + } + }) + } +} + +func TestFilePluginRepoGetSLIPlugin(t *testing.T) { + tests := map[string]struct { + pluginID string + fss func() []fs.FS + mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) + expPlugin pluginenginesli.SLIPlugin + expErr bool + }{ + "Having no files, should fail.": { + pluginID: "test", + fss: func() []fs.FS { return nil }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) {}, + expErr: true, + }, + + "Getting a correct plugin, should return the plugin.": { + pluginID: "p2", + fss: func() []fs.FS { + m := make(fstest.MapFS) + m["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + return []fs.FS{m} + }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(&pluginenginesli.SLIPlugin{ID: "p2"}, nil) + }, + expPlugin: pluginenginesli.SLIPlugin{ID: "p2"}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + mslopl := fsmock.NewSLOPluginLoader(t) + mslipl := fsmock.NewSLIPluginLoader(t) + test.mock(mslopl, mslipl) + + // Create repository and load plugins. + repo, err := storagefs.NewFilePluginRepo(log.Noop, mslipl, mslopl, test.fss()...) + require.NoError(err) + + plugin, err := repo.GetSLIPlugin(t.Context(), test.pluginID) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expPlugin, *plugin) + } + }) + } +} diff --git a/internal/storage/fs/sli_plugin.go b/internal/storage/fs/sli_plugin.go deleted file mode 100644 index efa23602..00000000 --- a/internal/storage/fs/sli_plugin.go +++ /dev/null @@ -1,197 +0,0 @@ -package fs - -import ( - "context" - "fmt" - "io/fs" - "os" - "path/filepath" - "regexp" - "sync" - - "github.com/slok/sloth/internal/log" - pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" -) - -type SLIPluginLoader interface { - LoadRawSLIPlugin(ctx context.Context, src string) (*pluginenginesli.SLIPlugin, error) -} - -//go:generate mockery --case underscore --output fsmock --outpkg fsmock --name SLIPluginLoader - -// FileManager knows how to manage files. -// TODO(slok): Use fs.FS. -type FileManager interface { - FindFiles(ctx context.Context, root string, matcher *regexp.Regexp) (paths []string, err error) - ReadFile(ctx context.Context, path string) (data []byte, err error) -} - -//go:generate mockery --case underscore --output fsmock --outpkg fsmock --name FileManager - -type fileManager struct{} - -func (f fileManager) FindFiles(ctx context.Context, root string, matcher *regexp.Regexp) ([]string, error) { - paths := []string{} - err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { - if err != nil { - return err - } - - if info.IsDir() { - return nil - } - - if matcher.MatchString(path) { - paths = append(paths, path) - } - - return nil - }) - - if err != nil { - return nil, fmt.Errorf("could not find files recursively: %w", err) - } - - return paths, nil -} - -func (f fileManager) ReadFile(_ context.Context, path string) ([]byte, error) { - return os.ReadFile(path) -} - -type FileSLIPluginRepoConfig struct { - FileManager FileManager - Paths []string - PluginLoader SLIPluginLoader - Logger log.Logger -} - -func (c *FileSLIPluginRepoConfig) defaults() error { - if c.FileManager == nil { - c.FileManager = fileManager{} - } - - if c.PluginLoader == nil { - c.PluginLoader = pluginenginesli.PluginLoader - } - - if c.Logger == nil { - c.Logger = log.Noop - } - c.Logger = c.Logger.WithValues(log.Kv{"svc": "storage.FileSLIPlugin"}) - - return nil -} - -func NewFileSLIPluginRepo(config FileSLIPluginRepoConfig) (*FileSLIPluginRepo, error) { - err := config.defaults() - if err != nil { - return nil, fmt.Errorf("invalid configuration: %w", err) - } - - f := &FileSLIPluginRepo{ - fileManager: config.FileManager, - pluginLoader: config.PluginLoader, - paths: config.Paths, - logger: config.Logger, - } - - err = f.Reload(context.Background()) - if err != nil { - return nil, fmt.Errorf("could not load plugins: %w", err) - } - - return f, nil -} - -// FileSLIPluginRepo will provide the plugins loaded from files. -// To be able to provide a simple and safe plugin system to the user we have set some -// rules/requirements that a plugin must implement: -// -// - The plugin must be in a `plugin.go` file inside a directory. -// - All the plugin must be in the `plugin.go` file. -// - The plugin can't import anything apart from the Go standard library. -// - `reflect` and `unsafe` packages can't be used. -// -// These rules provide multiple things: -// - Easy discovery of plugins without the need to provide extra data (import paths, path sanitization...). -// - Safety because we don't allow adding external packages easily. -// - Force keeping the plugins simple, small and without smart code. -// - Force avoiding DRY in small plugins and embrace WET to have independent plugins. -type FileSLIPluginRepo struct { - pluginLoader SLIPluginLoader - fileManager FileManager - paths []string - plugins map[string]pluginenginesli.SLIPlugin - mu sync.RWMutex - logger log.Logger -} - -var sliPluginNameRegex = regexp.MustCompile("plugin.go$") - -// Reload will reload all the plugins again from the paths. -func (f *FileSLIPluginRepo) Reload(ctx context.Context) error { - // Discover plugins. - paths := map[string]struct{}{} - for _, path := range f.paths { - discoveredPaths, err := f.fileManager.FindFiles(ctx, path, sliPluginNameRegex) - if err != nil { - return fmt.Errorf("could not discover SLI plugins: %w", err) - } - for _, dPath := range discoveredPaths { - paths[dPath] = struct{}{} - } - } - - // Load the plugins. - plugins := map[string]pluginenginesli.SLIPlugin{} - for path := range paths { - pluginData, err := f.fileManager.ReadFile(ctx, path) - if err != nil { - return fmt.Errorf("could not read %q plugin data: %w", path, err) - } - - // Create the plugin. - plugin, err := f.pluginLoader.LoadRawSLIPlugin(ctx, string(pluginData)) - if err != nil { - return fmt.Errorf("could not load %q plugin: %w", path, err) - } - - // Check collision. - _, ok := plugins[plugin.ID] - if ok { - return fmt.Errorf("2 or more plugins with the same %q ID have been loaded", plugin.ID) - } - - plugins[plugin.ID] = *plugin - f.logger.WithValues(log.Kv{"plugin-id": plugin.ID, "plugin-path": path}).Debugf("SLI plugin loaded") - } - - // Set loaded plugins. - f.mu.Lock() - f.plugins = plugins - f.mu.Unlock() - - f.logger.WithValues(log.Kv{"plugins": len(plugins)}).Infof("SLI plugins loaded") - - return nil -} - -func (f *FileSLIPluginRepo) ListSLIPlugins(ctx context.Context) (map[string]pluginenginesli.SLIPlugin, error) { - f.mu.RLock() - defer f.mu.RUnlock() - - return f.plugins, nil -} - -func (f *FileSLIPluginRepo) GetSLIPlugin(ctx context.Context, id string) (*pluginenginesli.SLIPlugin, error) { - f.mu.RLock() - defer f.mu.RUnlock() - - p, ok := f.plugins[id] - if !ok { - return nil, fmt.Errorf("plugin %q missing", id) - } - - return &p, nil -} diff --git a/internal/storage/fs/sli_plugin_test.go b/internal/storage/fs/sli_plugin_test.go deleted file mode 100644 index cd3ce8c1..00000000 --- a/internal/storage/fs/sli_plugin_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package fs_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" - "github.com/slok/sloth/internal/storage/fs" - "github.com/slok/sloth/internal/storage/fs/fsmock" -) - -func TestSLIPluginRepoListSLIPlugins(t *testing.T) { - tests := map[string]struct { - mock func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) - expPlugins map[string]pluginenginesli.SLIPlugin - expErr bool - }{ - "Having no files, should return empty list of plugins.": { - mock: func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) { - mfm.On("FindFiles", mock.Anything, "./", mock.Anything).Once().Return([]string{}, nil) - }, - expPlugins: map[string]pluginenginesli.SLIPlugin{}, - }, - - "Having multiple files, should return multiple plugins.": { - mock: func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) { - mfm.On("FindFiles", mock.Anything, "./", mock.Anything).Once().Return([]string{ - "./test_plugin_1.go", - "./test_plugin_2.go", - "./test2/test_plugin_3.go", - "./test3/test4/test_plugin_4.go", - }, nil) - - mfm.On("ReadFile", mock.Anything, "./test_plugin_1.go").Once().Return([]byte(`test1`), nil) - mfm.On("ReadFile", mock.Anything, "./test_plugin_2.go").Once().Return([]byte(`test2`), nil) - mfm.On("ReadFile", mock.Anything, "./test2/test_plugin_3.go").Once().Return([]byte(`test3`), nil) - mfm.On("ReadFile", mock.Anything, "./test3/test4/test_plugin_4.go").Once().Return([]byte(`test4`), nil) - - mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginenginesli.SLIPlugin{ID: "test1"}, nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginenginesli.SLIPlugin{ID: "test2"}, nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test3").Once().Return(&pluginenginesli.SLIPlugin{ID: "test3"}, nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test4").Once().Return(&pluginenginesli.SLIPlugin{ID: "test4"}, nil) - }, - expPlugins: map[string]pluginenginesli.SLIPlugin{ - "test1": {ID: "test1"}, - "test2": {ID: "test2"}, - "test3": {ID: "test3"}, - "test4": {ID: "test4"}, - }, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - mfm := fsmock.NewFileManager(t) - mpl := fsmock.NewSLIPluginLoader(t) - test.mock(mfm, mpl) - - // Create repository and load plugins. - config := fs.FileSLIPluginRepoConfig{ - FileManager: mfm, - PluginLoader: mpl, - Paths: []string{"./"}, - } - repo, err := fs.NewFileSLIPluginRepo(config) - require.NoError(err) - - plugins, err := repo.ListSLIPlugins(t.Context()) - if test.expErr { - assert.Error(err) - } else if assert.NoError(err) { - assert.Equal(test.expPlugins, plugins) - } - }) - } -} - -func TestSLIPluginRepoGetSLIPlugin(t *testing.T) { - tests := map[string]struct { - pluginID string - mock func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) - expPlugin pluginenginesli.SLIPlugin - expErr bool - }{ - "Having a missing plugin, should fail.": { - pluginID: "test3", - mock: func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) { - mfm.On("FindFiles", mock.Anything, "./", mock.Anything).Once().Return([]string{ - "./test_plugin_1.go", - "./test_plugin_2.go", - }, nil) - - mfm.On("ReadFile", mock.Anything, "./test_plugin_1.go").Once().Return([]byte(`test1`), nil) - mfm.On("ReadFile", mock.Anything, "./test_plugin_2.go").Once().Return([]byte(`test2`), nil) - - mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginenginesli.SLIPlugin{ID: "test1"}, nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginenginesli.SLIPlugin{ID: "test2"}, nil) - }, - expErr: true, - }, - - "Having a correct plugin, should return the plugin.": { - pluginID: "test2", - mock: func(mfm *fsmock.FileManager, mpl *fsmock.SLIPluginLoader) { - mfm.On("FindFiles", mock.Anything, "./", mock.Anything).Once().Return([]string{ - "./test_plugin_1.go", - "./test_plugin_2.go", - }, nil) - - mfm.On("ReadFile", mock.Anything, "./test_plugin_1.go").Once().Return([]byte(`test1`), nil) - mfm.On("ReadFile", mock.Anything, "./test_plugin_2.go").Once().Return([]byte(`test2`), nil) - - mpl.On("LoadRawSLIPlugin", mock.Anything, "test1").Once().Return(&pluginenginesli.SLIPlugin{ID: "test1"}, nil) - mpl.On("LoadRawSLIPlugin", mock.Anything, "test2").Once().Return(&pluginenginesli.SLIPlugin{ID: "test2"}, nil) - }, - expPlugin: pluginenginesli.SLIPlugin{ID: "test2"}, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - mfm := fsmock.NewFileManager(t) - mpl := fsmock.NewSLIPluginLoader(t) - test.mock(mfm, mpl) - - // Create repository and load plugins. - config := fs.FileSLIPluginRepoConfig{ - FileManager: mfm, - PluginLoader: mpl, - Paths: []string{"./"}, - } - repo, err := fs.NewFileSLIPluginRepo(config) - require.NoError(err) - - plugin, err := repo.GetSLIPlugin(t.Context(), test.pluginID) - if test.expErr { - assert.Error(err) - } else if assert.NoError(err) { - assert.Equal(test.expPlugin, *plugin) - } - }) - } -} diff --git a/internal/storage/fs/slo_plugin.go b/internal/storage/fs/slo_plugin.go deleted file mode 100644 index b1f17395..00000000 --- a/internal/storage/fs/slo_plugin.go +++ /dev/null @@ -1,124 +0,0 @@ -package fs - -import ( - "context" - "fmt" - "io/fs" - "regexp" - "sync" - - "github.com/slok/sloth/internal/log" - pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" - commonerrors "github.com/slok/sloth/pkg/common/errors" -) - -type SLOPluginLoader interface { - LoadRawPlugin(ctx context.Context, src string) (*pluginengineslo.Plugin, error) -} - -//go:generate mockery --case underscore --output fsmock --outpkg fsmock --name SLOPluginLoader - -type FileSLOPluginRepo struct { - fss []fs.FS - pluginLoader SLOPluginLoader - cache map[string]pluginengineslo.Plugin - logger log.Logger - mu sync.RWMutex -} - -// NewFileSLOPluginRepo returns a new FileSLOPluginRepo that loads plugins from the given file system. -// The plugin file should be called "plugin.go". -func NewFileSLOPluginRepo(logger log.Logger, pluginLoader SLOPluginLoader, fss ...fs.FS) (*FileSLOPluginRepo, error) { - r := &FileSLOPluginRepo{ - fss: fss, - pluginLoader: pluginLoader, - cache: map[string]pluginengineslo.Plugin{}, - logger: logger, - } - - err := r.Reload(context.Background()) - if err != nil { - return nil, fmt.Errorf("could not load plugins: %w", err) - } - - return r, nil -} - -var sloPluginNameRegex = regexp.MustCompile("plugin.go$") - -func (r *FileSLOPluginRepo) Reload(ctx context.Context) error { - plugins, err := r.loadPlugins(ctx, r.fss...) - if err != nil { - return fmt.Errorf("could not load plugins: %w", err) - } - - // Set loaded plugins. - r.mu.Lock() - r.cache = plugins - r.mu.Unlock() - - r.logger.WithValues(log.Kv{"plugins": len(plugins)}).Infof("SLO plugins loaded") - return nil -} - -func (r *FileSLOPluginRepo) GetSLOPlugin(ctx context.Context, id string) (*pluginengineslo.Plugin, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - p, ok := r.cache[id] - if !ok { - return nil, fmt.Errorf("plugin %q not found: %w", id, commonerrors.ErrNotFound) - } - - return &p, nil -} - -func (r *FileSLOPluginRepo) ListSLOPlugins(ctx context.Context) (map[string]pluginengineslo.Plugin, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - return r.cache, nil -} - -func (r *FileSLOPluginRepo) loadPlugins(ctx context.Context, fss ...fs.FS) (map[string]pluginengineslo.Plugin, error) { - allPlugins := map[string]pluginengineslo.Plugin{} - - for _, f := range fss { - err := fs.WalkDir(f, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - return nil - } - - if !sloPluginNameRegex.MatchString(path) { - return nil - } - - pluginData, err := fs.ReadFile(f, path) - if err != nil { - return fmt.Errorf("could not read %q plugin data: %w", path, err) - } - - plugin, err := r.pluginLoader.LoadRawPlugin(ctx, string(pluginData)) - if err != nil { - return fmt.Errorf("could not load %q plugin: %w", path, err) - } - - _, ok := allPlugins[plugin.ID] - if ok { - return fmt.Errorf("plugin %q already loaded", plugin.ID) - } - allPlugins[plugin.ID] = *plugin - r.logger.WithValues(log.Kv{"plugin-id": plugin.ID}).Debugf("SLO plugin discovered and loaded") - - return nil - }) - if err != nil { - return nil, fmt.Errorf("could not walk dir: %w", err) - } - } - - return allPlugins, nil -} diff --git a/internal/storage/fs/slo_plugin_test.go b/internal/storage/fs/slo_plugin_test.go deleted file mode 100644 index 8a36bfa2..00000000 --- a/internal/storage/fs/slo_plugin_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package fs_test - -import ( - "fmt" - "io/fs" - "testing" - "testing/fstest" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/slok/sloth/internal/log" - pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" - storagefs "github.com/slok/sloth/internal/storage/fs" - "github.com/slok/sloth/internal/storage/fs/fsmock" -) - -func TestFileSLOPluginRepoListSLOPlugins(t *testing.T) { - tests := map[string]struct { - fss func() []fs.FS - mock func(mpl *fsmock.SLOPluginLoader) - expPlugins map[string]pluginengineslo.Plugin - expLoadErr bool - expErr bool - }{ - "Having no files, should return empty list of plugins.": { - fss: func() []fs.FS { return nil }, - mock: func(mpl *fsmock.SLOPluginLoader) {}, - expPlugins: map[string]pluginengineslo.Plugin{}, - }, - - "Having plugins in multiple FS and directories, should return all plugins.": { - fss: func() []fs.FS { - m1 := make(fstest.MapFS) - m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} - m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} - m1["m1/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p3")} - - m2 := make(fstest.MapFS) - m2["m2/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p4")} - m2["m2/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p5")} - m2["m2/plugin-test.go"] = &fstest.MapFile{Data: []byte("p8")} // Ignored. - - m3 := make(fstest.MapFS) - m3["m3/plx/pl3/plugin.go"] = &fstest.MapFile{Data: []byte("p6")} - m3["m3/plx/pl3/plugin.yaml"] = &fstest.MapFile{Data: []byte("p7")} // Ignored. - - return []fs.FS{m1, m2, m3} - }, - mock: func(mpl *fsmock.SLOPluginLoader) { - mpl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) - mpl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(&pluginengineslo.Plugin{ID: "p2"}, nil) - mpl.On("LoadRawPlugin", mock.Anything, "p3").Once().Return(&pluginengineslo.Plugin{ID: "p3"}, nil) - mpl.On("LoadRawPlugin", mock.Anything, "p4").Once().Return(&pluginengineslo.Plugin{ID: "p4"}, nil) - mpl.On("LoadRawPlugin", mock.Anything, "p5").Once().Return(&pluginengineslo.Plugin{ID: "p5"}, nil) - mpl.On("LoadRawPlugin", mock.Anything, "p6").Once().Return(&pluginengineslo.Plugin{ID: "p6"}, nil) - }, - expPlugins: map[string]pluginengineslo.Plugin{ - "p1": {ID: "p1"}, - "p2": {ID: "p2"}, - "p3": {ID: "p3"}, - "p4": {ID: "p4"}, - "p5": {ID: "p5"}, - "p6": {ID: "p6"}, - }, - }, - - "Having a plugin loaded with the same ID multiple times should fail.": { - fss: func() []fs.FS { - m1 := make(fstest.MapFS) - m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} - m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} - - return []fs.FS{m1} - }, - mock: func(mpl *fsmock.SLOPluginLoader) { - mpl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) - mpl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) - - }, - expLoadErr: true, - }, - - "Having an error while loading a plugin, should fail.": { - fss: func() []fs.FS { - m1 := make(fstest.MapFS) - m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} - m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} - - return []fs.FS{m1} - }, - mock: func(mpl *fsmock.SLOPluginLoader) { - mpl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) - mpl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) - - }, - expLoadErr: true, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - - mpl := fsmock.NewSLOPluginLoader(t) - test.mock(mpl) - - // Create repository and load plugins. - repo, err := storagefs.NewFileSLOPluginRepo(log.Noop, mpl, test.fss()...) - if test.expLoadErr { - assert.Error(err) - return - } - assert.NoError(err) - - plugins, err := repo.ListSLOPlugins(t.Context()) - if test.expErr { - assert.Error(err) - } else if assert.NoError(err) { - assert.Equal(test.expPlugins, plugins) - } - }) - } -} - -func TestFileSLOPluginRepoGetSLOPlugin(t *testing.T) { - tests := map[string]struct { - pluginID string - fss func() []fs.FS - mock func(mpl *fsmock.SLOPluginLoader) - expPlugin pluginengineslo.Plugin - expErr bool - }{ - "Having no files, should return empty list of plugins.": { - pluginID: "test", - fss: func() []fs.FS { return nil }, - mock: func(mpl *fsmock.SLOPluginLoader) {}, - expErr: true, - }, - - "Getting a correct plugin, should return the plugin.": { - pluginID: "p2", - fss: func() []fs.FS { - m := make(fstest.MapFS) - m["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} - m["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} - return []fs.FS{m} - }, - mock: func(mpl *fsmock.SLOPluginLoader) { - mpl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) - mpl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(&pluginengineslo.Plugin{ID: "p2"}, nil) - }, - expPlugin: pluginengineslo.Plugin{ID: "p2"}, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - mpl := fsmock.NewSLOPluginLoader(t) - test.mock(mpl) - - // Create repository and load plugins. - repo, err := storagefs.NewFileSLOPluginRepo(log.Noop, mpl, test.fss()...) - require.NoError(err) - - plugin, err := repo.GetSLOPlugin(t.Context(), test.pluginID) - if test.expErr { - assert.Error(err) - } else if assert.NoError(err) { - assert.Equal(test.expPlugin, *plugin) - } - }) - } -} diff --git a/test/integration/k8scontroller/k8scontroller_test.go b/test/integration/k8scontroller/k8scontroller_test.go index 65658473..0fe15e1d 100644 --- a/test/integration/k8scontroller/k8scontroller_test.go +++ b/test/integration/k8scontroller/k8scontroller_test.go @@ -224,7 +224,7 @@ func TestKubernetesControllerPromOperatorGenerate(t *testing.T) { args := []string{ "--metrics-listen-addr=:0", "--hot-reload-addr=:0", - "--sli-plugins-path=./", + "--plugins-path=./", fmt.Sprintf("--namespace=%s", ns), fmt.Sprintf("--default-slo-period=%s", test.sloPeriod), } diff --git a/test/integration/prometheus/helpers.go b/test/integration/prometheus/helpers.go index 8c0371a7..d7d69374 100644 --- a/test/integration/prometheus/helpers.go +++ b/test/integration/prometheus/helpers.go @@ -48,8 +48,7 @@ func NewConfig(t *testing.T) Config { func RunSlothGenerate(ctx context.Context, config Config, cmdArgs string) (stdout, stderr []byte, err error) { env := []string{ - fmt.Sprintf("SLOTH_SLI_PLUGINS_PATH=%s", "./sli_plugins"), - fmt.Sprintf("SLOTH_SLO_PLUGINS_PATH=%s", "./slo_plugins"), + fmt.Sprintf("SLOTH_PLUGINS_PATH=%s", "./plugins"), } return testutils.RunSloth(ctx, env, config.Binary, fmt.Sprintf("generate %s", cmdArgs), true) @@ -57,8 +56,7 @@ func RunSlothGenerate(ctx context.Context, config Config, cmdArgs string) (stdou func RunSlothValidate(ctx context.Context, config Config, cmdArgs string) (stdout, stderr []byte, err error) { env := []string{ - fmt.Sprintf("SLOTH_SLI_PLUGINS_PATH=%s", "./sli_plugins"), - fmt.Sprintf("SLOTH_SLO_PLUGINS_PATH=%s", "./slo_plugins"), + fmt.Sprintf("SLOTH_PLUGINS_PATH=%s", "./plugins"), } return testutils.RunSloth(ctx, env, config.Binary, fmt.Sprintf("validate %s", cmdArgs), true) diff --git a/test/integration/prometheus/sli_plugins/plugin1/plugin.go b/test/integration/prometheus/plugins/sli/plugin1/plugin.go similarity index 100% rename from test/integration/prometheus/sli_plugins/plugin1/plugin.go rename to test/integration/prometheus/plugins/sli/plugin1/plugin.go diff --git a/test/integration/prometheus/slo_plugins/plugin1/plugin.go b/test/integration/prometheus/plugins/slo/plugin1/plugin.go similarity index 100% rename from test/integration/prometheus/slo_plugins/plugin1/plugin.go rename to test/integration/prometheus/plugins/slo/plugin1/plugin.go From 9e6630c0a5db172b723a4dfe877bbd6903c83027 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 12 Apr 2025 09:14:47 +0200 Subject: [PATCH 048/173] Migrate to mockery v3 Signed-off-by: Xabier Larrakoetxea --- .mockery.yml | 9 + docker/dev/Dockerfile | 4 +- docker/prod/Dockerfile | 2 +- internal/app/generate/generate.go | 2 - internal/app/generate/generatemock/mocks.go | 96 ++++++++++ .../generatemock/slo_plugin_getter.go | 60 ------ internal/storage/fs/fsmock/file_manager.go | 90 --------- internal/storage/fs/fsmock/mocks.go | 181 ++++++++++++++++++ .../storage/fs/fsmock/sli_plugin_loader.go | 60 ------ .../storage/fs/fsmock/slo_plugin_loader.go | 60 ------ internal/storage/fs/plugin.go | 4 - scripts/gogen.sh | 3 +- 12 files changed, 291 insertions(+), 280 deletions(-) create mode 100644 .mockery.yml create mode 100644 internal/app/generate/generatemock/mocks.go delete mode 100644 internal/app/generate/generatemock/slo_plugin_getter.go delete mode 100644 internal/storage/fs/fsmock/file_manager.go create mode 100644 internal/storage/fs/fsmock/mocks.go delete mode 100644 internal/storage/fs/fsmock/sli_plugin_loader.go delete mode 100644 internal/storage/fs/fsmock/slo_plugin_loader.go diff --git a/.mockery.yml b/.mockery.yml new file mode 100644 index 00000000..dafcf79b --- /dev/null +++ b/.mockery.yml @@ -0,0 +1,9 @@ +dir: '{{.InterfaceDir}}/{{.SrcPackageName}}mock' +filename: mocks.go +force-file-write: true +structname: '{{.InterfaceName}}' +pkgname: '{{.SrcPackageName}}mock' +template: testify +packages: + github.com/slok/sloth/internal/app/generate: {interfaces: {SLOPluginGetter}} + github.com/slok/sloth/internal/storage/fs: {interfaces: {SLIPluginLoader,SLOPluginLoader}} diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 7cde5fdc..a11d422f 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -1,9 +1,9 @@ FROM golang:1.24 -LABEL org.opencontainers.image.source https://github.com/slok/sloth +LABEL org.opencontainers.image.source=https://github.com/slok/sloth ARG GOLANGCI_LINT_VERSION="1.64.8" -ARG MOCKERY_VERSION="2.53.3" +ARG MOCKERY_VERSION="3.1.0" ARG GOMARKDOC_VERSION="1.1.0" ARG HELM_VERSION="3.17.0" ARG YAEGI_VERSION="0.16.1" diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 5a930241..4e4b1c2a 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -3,7 +3,7 @@ ARG ARCH FROM golang:1.24.2-alpine as build-stage -LABEL org.opencontainers.image.source https://github.com/slok/sloth +LABEL org.opencontainers.image.source=https://github.com/slok/sloth RUN apk --no-cache add \ g++ \ diff --git a/internal/app/generate/generate.go b/internal/app/generate/generate.go index e9dff277..063a1fe3 100644 --- a/internal/app/generate/generate.go +++ b/internal/app/generate/generate.go @@ -34,8 +34,6 @@ type SLOPluginGetter interface { GetSLOPlugin(ctx context.Context, id string) (*pluginengineslo.Plugin, error) } -//go:generate mockery --case underscore --output generatemock --outpkg generatemock --name SLOPluginGetter - // ServiceConfig is the application service configuration. type ServiceConfig struct { AlertGenerator AlertGenerator diff --git a/internal/app/generate/generatemock/mocks.go b/internal/app/generate/generatemock/mocks.go new file mode 100644 index 00000000..39f73cca --- /dev/null +++ b/internal/app/generate/generatemock/mocks.go @@ -0,0 +1,96 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package generatemock + +import ( + "context" + + "github.com/slok/sloth/internal/pluginengine/slo" + mock "github.com/stretchr/testify/mock" +) + +// NewSLOPluginGetter creates a new instance of SLOPluginGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSLOPluginGetter(t interface { + mock.TestingT + Cleanup(func()) +}) *SLOPluginGetter { + mock := &SLOPluginGetter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// SLOPluginGetter is an autogenerated mock type for the SLOPluginGetter type +type SLOPluginGetter struct { + mock.Mock +} + +type SLOPluginGetter_Expecter struct { + mock *mock.Mock +} + +func (_m *SLOPluginGetter) EXPECT() *SLOPluginGetter_Expecter { + return &SLOPluginGetter_Expecter{mock: &_m.Mock} +} + +// GetSLOPlugin provides a mock function for the type SLOPluginGetter +func (_mock *SLOPluginGetter) GetSLOPlugin(ctx context.Context, id string) (*slo.Plugin, error) { + ret := _mock.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetSLOPlugin") + } + + var r0 *slo.Plugin + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*slo.Plugin, error)); ok { + return returnFunc(ctx, id) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *slo.Plugin); ok { + r0 = returnFunc(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*slo.Plugin) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, id) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// SLOPluginGetter_GetSLOPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSLOPlugin' +type SLOPluginGetter_GetSLOPlugin_Call struct { + *mock.Call +} + +// GetSLOPlugin is a helper method to define mock.On call +// - ctx +// - id +func (_e *SLOPluginGetter_Expecter) GetSLOPlugin(ctx interface{}, id interface{}) *SLOPluginGetter_GetSLOPlugin_Call { + return &SLOPluginGetter_GetSLOPlugin_Call{Call: _e.mock.On("GetSLOPlugin", ctx, id)} +} + +func (_c *SLOPluginGetter_GetSLOPlugin_Call) Run(run func(ctx context.Context, id string)) *SLOPluginGetter_GetSLOPlugin_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *SLOPluginGetter_GetSLOPlugin_Call) Return(plugin *slo.Plugin, err error) *SLOPluginGetter_GetSLOPlugin_Call { + _c.Call.Return(plugin, err) + return _c +} + +func (_c *SLOPluginGetter_GetSLOPlugin_Call) RunAndReturn(run func(ctx context.Context, id string) (*slo.Plugin, error)) *SLOPluginGetter_GetSLOPlugin_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/app/generate/generatemock/slo_plugin_getter.go b/internal/app/generate/generatemock/slo_plugin_getter.go deleted file mode 100644 index e9df51b9..00000000 --- a/internal/app/generate/generatemock/slo_plugin_getter.go +++ /dev/null @@ -1,60 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package generatemock - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - - slo "github.com/slok/sloth/internal/pluginengine/slo" -) - -// SLOPluginGetter is an autogenerated mock type for the SLOPluginGetter type -type SLOPluginGetter struct { - mock.Mock -} - -// GetSLOPlugin provides a mock function with given fields: ctx, id -func (_m *SLOPluginGetter) GetSLOPlugin(ctx context.Context, id string) (*slo.Plugin, error) { - ret := _m.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for GetSLOPlugin") - } - - var r0 *slo.Plugin - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*slo.Plugin, error)); ok { - return rf(ctx, id) - } - if rf, ok := ret.Get(0).(func(context.Context, string) *slo.Plugin); ok { - r0 = rf(ctx, id) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*slo.Plugin) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewSLOPluginGetter creates a new instance of SLOPluginGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewSLOPluginGetter(t interface { - mock.TestingT - Cleanup(func()) -}) *SLOPluginGetter { - mock := &SLOPluginGetter{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/storage/fs/fsmock/file_manager.go b/internal/storage/fs/fsmock/file_manager.go deleted file mode 100644 index 0cc40d54..00000000 --- a/internal/storage/fs/fsmock/file_manager.go +++ /dev/null @@ -1,90 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package fsmock - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - - regexp "regexp" -) - -// FileManager is an autogenerated mock type for the FileManager type -type FileManager struct { - mock.Mock -} - -// FindFiles provides a mock function with given fields: ctx, root, matcher -func (_m *FileManager) FindFiles(ctx context.Context, root string, matcher *regexp.Regexp) ([]string, error) { - ret := _m.Called(ctx, root, matcher) - - if len(ret) == 0 { - panic("no return value specified for FindFiles") - } - - var r0 []string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, *regexp.Regexp) ([]string, error)); ok { - return rf(ctx, root, matcher) - } - if rf, ok := ret.Get(0).(func(context.Context, string, *regexp.Regexp) []string); ok { - r0 = rf(ctx, root, matcher) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string, *regexp.Regexp) error); ok { - r1 = rf(ctx, root, matcher) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ReadFile provides a mock function with given fields: ctx, path -func (_m *FileManager) ReadFile(ctx context.Context, path string) ([]byte, error) { - ret := _m.Called(ctx, path) - - if len(ret) == 0 { - panic("no return value specified for ReadFile") - } - - var r0 []byte - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) ([]byte, error)); ok { - return rf(ctx, path) - } - if rf, ok := ret.Get(0).(func(context.Context, string) []byte); ok { - r0 = rf(ctx, path) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, path) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewFileManager creates a new instance of FileManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewFileManager(t interface { - mock.TestingT - Cleanup(func()) -}) *FileManager { - mock := &FileManager{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/storage/fs/fsmock/mocks.go b/internal/storage/fs/fsmock/mocks.go new file mode 100644 index 00000000..e90548da --- /dev/null +++ b/internal/storage/fs/fsmock/mocks.go @@ -0,0 +1,181 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package fsmock + +import ( + "context" + + "github.com/slok/sloth/internal/pluginengine/sli" + "github.com/slok/sloth/internal/pluginengine/slo" + mock "github.com/stretchr/testify/mock" +) + +// NewSLIPluginLoader creates a new instance of SLIPluginLoader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSLIPluginLoader(t interface { + mock.TestingT + Cleanup(func()) +}) *SLIPluginLoader { + mock := &SLIPluginLoader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// SLIPluginLoader is an autogenerated mock type for the SLIPluginLoader type +type SLIPluginLoader struct { + mock.Mock +} + +type SLIPluginLoader_Expecter struct { + mock *mock.Mock +} + +func (_m *SLIPluginLoader) EXPECT() *SLIPluginLoader_Expecter { + return &SLIPluginLoader_Expecter{mock: &_m.Mock} +} + +// LoadRawSLIPlugin provides a mock function for the type SLIPluginLoader +func (_mock *SLIPluginLoader) LoadRawSLIPlugin(ctx context.Context, src string) (*sli.SLIPlugin, error) { + ret := _mock.Called(ctx, src) + + if len(ret) == 0 { + panic("no return value specified for LoadRawSLIPlugin") + } + + var r0 *sli.SLIPlugin + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*sli.SLIPlugin, error)); ok { + return returnFunc(ctx, src) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *sli.SLIPlugin); ok { + r0 = returnFunc(ctx, src) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sli.SLIPlugin) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, src) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// SLIPluginLoader_LoadRawSLIPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadRawSLIPlugin' +type SLIPluginLoader_LoadRawSLIPlugin_Call struct { + *mock.Call +} + +// LoadRawSLIPlugin is a helper method to define mock.On call +// - ctx +// - src +func (_e *SLIPluginLoader_Expecter) LoadRawSLIPlugin(ctx interface{}, src interface{}) *SLIPluginLoader_LoadRawSLIPlugin_Call { + return &SLIPluginLoader_LoadRawSLIPlugin_Call{Call: _e.mock.On("LoadRawSLIPlugin", ctx, src)} +} + +func (_c *SLIPluginLoader_LoadRawSLIPlugin_Call) Run(run func(ctx context.Context, src string)) *SLIPluginLoader_LoadRawSLIPlugin_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *SLIPluginLoader_LoadRawSLIPlugin_Call) Return(sLIPlugin *sli.SLIPlugin, err error) *SLIPluginLoader_LoadRawSLIPlugin_Call { + _c.Call.Return(sLIPlugin, err) + return _c +} + +func (_c *SLIPluginLoader_LoadRawSLIPlugin_Call) RunAndReturn(run func(ctx context.Context, src string) (*sli.SLIPlugin, error)) *SLIPluginLoader_LoadRawSLIPlugin_Call { + _c.Call.Return(run) + return _c +} + +// NewSLOPluginLoader creates a new instance of SLOPluginLoader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSLOPluginLoader(t interface { + mock.TestingT + Cleanup(func()) +}) *SLOPluginLoader { + mock := &SLOPluginLoader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// SLOPluginLoader is an autogenerated mock type for the SLOPluginLoader type +type SLOPluginLoader struct { + mock.Mock +} + +type SLOPluginLoader_Expecter struct { + mock *mock.Mock +} + +func (_m *SLOPluginLoader) EXPECT() *SLOPluginLoader_Expecter { + return &SLOPluginLoader_Expecter{mock: &_m.Mock} +} + +// LoadRawPlugin provides a mock function for the type SLOPluginLoader +func (_mock *SLOPluginLoader) LoadRawPlugin(ctx context.Context, src string) (*slo.Plugin, error) { + ret := _mock.Called(ctx, src) + + if len(ret) == 0 { + panic("no return value specified for LoadRawPlugin") + } + + var r0 *slo.Plugin + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*slo.Plugin, error)); ok { + return returnFunc(ctx, src) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *slo.Plugin); ok { + r0 = returnFunc(ctx, src) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*slo.Plugin) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, src) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// SLOPluginLoader_LoadRawPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadRawPlugin' +type SLOPluginLoader_LoadRawPlugin_Call struct { + *mock.Call +} + +// LoadRawPlugin is a helper method to define mock.On call +// - ctx +// - src +func (_e *SLOPluginLoader_Expecter) LoadRawPlugin(ctx interface{}, src interface{}) *SLOPluginLoader_LoadRawPlugin_Call { + return &SLOPluginLoader_LoadRawPlugin_Call{Call: _e.mock.On("LoadRawPlugin", ctx, src)} +} + +func (_c *SLOPluginLoader_LoadRawPlugin_Call) Run(run func(ctx context.Context, src string)) *SLOPluginLoader_LoadRawPlugin_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *SLOPluginLoader_LoadRawPlugin_Call) Return(plugin *slo.Plugin, err error) *SLOPluginLoader_LoadRawPlugin_Call { + _c.Call.Return(plugin, err) + return _c +} + +func (_c *SLOPluginLoader_LoadRawPlugin_Call) RunAndReturn(run func(ctx context.Context, src string) (*slo.Plugin, error)) *SLOPluginLoader_LoadRawPlugin_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/storage/fs/fsmock/sli_plugin_loader.go b/internal/storage/fs/fsmock/sli_plugin_loader.go deleted file mode 100644 index fbadae40..00000000 --- a/internal/storage/fs/fsmock/sli_plugin_loader.go +++ /dev/null @@ -1,60 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package fsmock - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - - sli "github.com/slok/sloth/internal/pluginengine/sli" -) - -// SLIPluginLoader is an autogenerated mock type for the SLIPluginLoader type -type SLIPluginLoader struct { - mock.Mock -} - -// LoadRawSLIPlugin provides a mock function with given fields: ctx, src -func (_m *SLIPluginLoader) LoadRawSLIPlugin(ctx context.Context, src string) (*sli.SLIPlugin, error) { - ret := _m.Called(ctx, src) - - if len(ret) == 0 { - panic("no return value specified for LoadRawSLIPlugin") - } - - var r0 *sli.SLIPlugin - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*sli.SLIPlugin, error)); ok { - return rf(ctx, src) - } - if rf, ok := ret.Get(0).(func(context.Context, string) *sli.SLIPlugin); ok { - r0 = rf(ctx, src) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*sli.SLIPlugin) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, src) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewSLIPluginLoader creates a new instance of SLIPluginLoader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewSLIPluginLoader(t interface { - mock.TestingT - Cleanup(func()) -}) *SLIPluginLoader { - mock := &SLIPluginLoader{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/storage/fs/fsmock/slo_plugin_loader.go b/internal/storage/fs/fsmock/slo_plugin_loader.go deleted file mode 100644 index d1b4cbf4..00000000 --- a/internal/storage/fs/fsmock/slo_plugin_loader.go +++ /dev/null @@ -1,60 +0,0 @@ -// Code generated by mockery v2.53.3. DO NOT EDIT. - -package fsmock - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - - slo "github.com/slok/sloth/internal/pluginengine/slo" -) - -// SLOPluginLoader is an autogenerated mock type for the SLOPluginLoader type -type SLOPluginLoader struct { - mock.Mock -} - -// LoadRawPlugin provides a mock function with given fields: ctx, src -func (_m *SLOPluginLoader) LoadRawPlugin(ctx context.Context, src string) (*slo.Plugin, error) { - ret := _m.Called(ctx, src) - - if len(ret) == 0 { - panic("no return value specified for LoadRawPlugin") - } - - var r0 *slo.Plugin - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (*slo.Plugin, error)); ok { - return rf(ctx, src) - } - if rf, ok := ret.Get(0).(func(context.Context, string) *slo.Plugin); ok { - r0 = rf(ctx, src) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*slo.Plugin) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, src) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewSLOPluginLoader creates a new instance of SLOPluginLoader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewSLOPluginLoader(t interface { - mock.TestingT - Cleanup(func()) -}) *SLOPluginLoader { - mock := &SLOPluginLoader{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/storage/fs/plugin.go b/internal/storage/fs/plugin.go index 5c782f6c..6b16955d 100644 --- a/internal/storage/fs/plugin.go +++ b/internal/storage/fs/plugin.go @@ -17,14 +17,10 @@ type SLIPluginLoader interface { LoadRawSLIPlugin(ctx context.Context, src string) (*pluginenginesli.SLIPlugin, error) } -//go:generate mockery --case underscore --output fsmock --outpkg fsmock --name SLIPluginLoader - type SLOPluginLoader interface { LoadRawPlugin(ctx context.Context, src string) (*pluginengineslo.Plugin, error) } -//go:generate mockery --case underscore --output fsmock --outpkg fsmock --name SLOPluginLoader - type FilePluginRepo struct { fss []fs.FS sloPluginLoader SLOPluginLoader diff --git a/scripts/gogen.sh b/scripts/gogen.sh index 803ac8a6..b758d6ec 100755 --- a/scripts/gogen.sh +++ b/scripts/gogen.sh @@ -3,4 +3,5 @@ set -o errexit set -o nounset -go generate ./... \ No newline at end of file +go generate ./... +mockery From 8a4a288823c1ddea0028b8be2869c6f664ebbeb6 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sun, 13 Apr 2025 12:19:17 +0200 Subject: [PATCH 049/173] Add support for SLO plugins on Kubernetes API Signed-off-by: Xabier Larrakoetxea --- ...loth.slok.dev_prometheusservicelevels.yaml | 71 +++ .../_gen/slo-plugin-k8s-getting-started.yml | 259 +++++++++ examples/slo-plugin-k8s-getting-started.yml | 62 +++ internal/storage/io/k8s_sloth.go | 24 + internal/storage/io/k8s_sloth_test.go | 40 ++ internal/storage/io/sloth_test.go | 12 +- pkg/kubernetes/api/sloth/v1/README.md | 162 ++++-- pkg/kubernetes/api/sloth/v1/types.go | 41 ++ .../api/sloth/v1/zz_generated.deepcopy.go | 56 ++ .../sloth/v1/prometheusservicelevelspec.go | 15 +- .../gen/applyconfiguration/sloth/v1/slo.go | 21 +- .../applyconfiguration/sloth/v1/sloplugin.go | 45 ++ .../applyconfiguration/sloth/v1/sloplugins.go | 28 + .../gen/applyconfiguration/utils.go | 4 + ...loth.slok.dev_prometheusservicelevels.yaml | 71 +++ ...plugin_test.go => exp_sli_plugins_test.go} | 4 +- .../k8scontroller/exp_slo_plugins_test.go | 497 ++++++++++++++++++ .../k8scontroller/k8scontroller_test.go | 33 +- .../{plugin => plugins/sli/plugin1}/plugin.go | 0 .../plugins/slo/plugin1/plugin.go | 46 ++ test/integration/prometheus/generate_test.go | 5 + .../testdata/in-slo-plugin-k8s.yaml | 39 ++ .../testdata/out-slo-plugin-k8s.yaml.tpl | 187 +++++++ 23 files changed, 1665 insertions(+), 57 deletions(-) create mode 100644 examples/_gen/slo-plugin-k8s-getting-started.yml create mode 100644 examples/slo-plugin-k8s-getting-started.yml create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugin.go create mode 100644 pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugins.go rename test/integration/k8scontroller/{exp_plugin_test.go => exp_sli_plugins_test.go} (98%) create mode 100644 test/integration/k8scontroller/exp_slo_plugins_test.go rename test/integration/k8scontroller/{plugin => plugins/sli/plugin1}/plugin.go (100%) create mode 100644 test/integration/k8scontroller/plugins/slo/plugin1/plugin.go create mode 100644 test/integration/prometheus/testdata/in-slo-plugin-k8s.yaml create mode 100644 test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl diff --git a/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml b/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml index 83cd4c98..ffdf7964 100644 --- a/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml +++ b/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml @@ -78,6 +78,41 @@ spec: service: description: Service is the application of the SLOs. type: string + sloPlugins: + description: SLOPlugins will be added to the SLO generation plugin + chain of all SLOs. + properties: + chain: + description: chain ths the list of plugin chain to add to the + SLO generation. + items: + description: SLOPlugin is a plugin that will be used on the + chain of plugins for the SLO generation. + properties: + config: + description: Config is the configuration used on the plugin + instance creation. + type: object + x-kubernetes-preserve-unknown-fields: true + id: + description: ID is the ID of the plugin to load . + type: string + priority: + description: |- + Priority is the priority of the plugin in the chain. The lower the number + the higher the priority. The first plugin will be the one with the lowest + priority. + The default plugins loaded by Sloth use `0` priority. If you want to + execute plugins before the default ones, you can use negative priority. + It is recommended to use round gaps of numbers like 10, 100, 1000, -200, -1000... + type: integer + required: + - id + type: object + type: array + required: + - chain + type: object slos: description: SLOs are the SLOs of the service. items: @@ -173,6 +208,42 @@ spec: description: Objective is target of the SLO the percentage (0, 100] (e.g 99.9). type: number + plugins: + description: |- + Plugins will be added along the group SLO plugins declared in the spec root level + and Sloth default plugins. + properties: + chain: + description: chain ths the list of plugin chain to add to + the SLO generation. + items: + description: SLOPlugin is a plugin that will be used on + the chain of plugins for the SLO generation. + properties: + config: + description: Config is the configuration used on the + plugin instance creation. + type: object + x-kubernetes-preserve-unknown-fields: true + id: + description: ID is the ID of the plugin to load . + type: string + priority: + description: |- + Priority is the priority of the plugin in the chain. The lower the number + the higher the priority. The first plugin will be the one with the lowest + priority. + The default plugins loaded by Sloth use `0` priority. If you want to + execute plugins before the default ones, you can use negative priority. + It is recommended to use round gaps of numbers like 10, 100, 1000, -200, -1000... + type: integer + required: + - id + type: object + type: array + required: + - chain + type: object sli: description: SLI is the indicator (service level indicator) for this specific SLO. diff --git a/examples/_gen/slo-plugin-k8s-getting-started.yml b/examples/_gen/slo-plugin-k8s-getting-started.yml new file mode 100644 index 00000000..de14e579 --- /dev/null +++ b/examples/_gen/slo-plugin-k8s-getting-started.yml @@ -0,0 +1,259 @@ + +--- +# Code generated by Sloth (dev): https://github.com/slok/sloth. +# DO NOT EDIT. + +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/component: SLO + app.kubernetes.io/managed-by: sloth + name: sloth-slo-my-service + namespace: monitoring +spec: + groups: + - name: sloth-slo-sli-recordings-myservice-requests-availability + rules: + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[5m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[5m]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 5m + tier: "2" + record: slo:sli_error:ratio_rate5m + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[30m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[30m]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 30m + tier: "2" + record: slo:sli_error:ratio_rate30m + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 1h + tier: "2" + record: slo:sli_error:ratio_rate1h + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[2h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[2h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 2h + tier: "2" + record: slo:sli_error:ratio_rate2h + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[6h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[6h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 6h + tier: "2" + record: slo:sli_error:ratio_rate6h + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1d]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 1d + tier: "2" + record: slo:sli_error:ratio_rate1d + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[3d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[3d]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 3d + tier: "2" + record: slo:sli_error:ratio_rate3d + - expr: | + sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"}[30d]) + / ignoring (sloth_window) + count_over_time(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"}[30d]) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + sloth_window: 30d + tier: "2" + record: slo:sli_error:ratio_rate30d + - name: sloth-slo-meta-recordings-myservice-requests-availability + rules: + - expr: vector(0.9990000000000001) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + record: slo:objective:ratio + - expr: vector(1-0.9990000000000001) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + record: slo:error_budget:ratio + - expr: vector(30) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + record: slo:time_period:days + - expr: | + slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + record: slo:current_burn_rate:ratio + - expr: | + slo:sli_error:ratio_rate30d{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + record: slo:period_burn_rate:ratio + - expr: 1 - slo:period_burn_rate:ratio{sloth_id="myservice-requests-availability", + sloth_service="myservice", sloth_slo="requests-availability"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_service: myservice + sloth_slo: requests-availability + tier: "2" + record: slo:period_error_budget_remaining:ratio + - expr: vector(1) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability + sloth_mode: cli-gen-k8s + sloth_objective: "99.9" + sloth_service: myservice + sloth_slo: requests-availability + sloth_spec: sloth.slok.dev/v1 + sloth_version: dev + tier: "2" + record: sloth_slo_info + - name: sloth-slo-alerts-myservice-requests-availability + rules: + - alert: MyServiceHighErrorRate + annotations: + summary: High error rate on 'myservice' requests responses + title: (page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. + expr: | + ( + max(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (14.4 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (14.4 * 0.0009999999999999432)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate30m{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (6 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate6h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (6 * 0.0009999999999999432)) without (sloth_window) + ) + labels: + category: availability + routing_key: myteam + severity: pageteam + sloth_severity: page + - alert: MyServiceHighErrorRate + annotations: + summary: High error rate on 'myservice' requests responses + title: (ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error + budget burn rate is too fast. + expr: | + ( + max(slo:sli_error:ratio_rate2h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (3 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1d{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (3 * 0.0009999999999999432)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate6h{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (1 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate3d{sloth_id="myservice-requests-availability", sloth_service="myservice", sloth_slo="requests-availability"} > (1 * 0.0009999999999999432)) without (sloth_window) + ) + labels: + category: availability + severity: slack + slack_channel: '#alerts-myteam' + sloth_severity: ticket diff --git a/examples/slo-plugin-k8s-getting-started.yml b/examples/slo-plugin-k8s-getting-started.yml new file mode 100644 index 00000000..7b79a92a --- /dev/null +++ b/examples/slo-plugin-k8s-getting-started.yml @@ -0,0 +1,62 @@ +# This example shows the same example as getting-started.yml but using Sloth Kubernetes CRD. +# It will generate the Prometheus rules in a Kubernetes prometheus-operator PrometheusRules CRD. +# +# `sloth generate -i ./examples/k8s-getting-started.yml` +# +apiVersion: sloth.slok.dev/v1 +kind: PrometheusServiceLevel +metadata: + name: sloth-slo-my-service + namespace: monitoring +spec: + service: "myservice" + labels: + owner: "myteam" + repo: "myorg/myservice" + tier: "2" + sloPlugins: + chain: + - id: "sloth.dev/core/debug/v1" + priority: 9999999 + config: {msg: "Plugin 99"} + - id: "sloth.dev/core/debug/v1" + priority: -999999 + config: {msg: "Plugin 0"} + slos: + - name: "requests-availability" + objective: 99.9 + description: "Common SLO based on availability for HTTP request responses." + plugins: + chain: + - id: "sloth.dev/core/debug/v1" + priority: 1050 + config: {msg: "Plugin 5"} + - id: "sloth.dev/core/debug/v1" + priority: -1000 + config: {msg: "Plugin 1"} + - id: "sloth.dev/core/debug/v1" + priority: 1000 + config: {msg: "Plugin 4"} + - id: "sloth.dev/core/debug/v1" + priority: -200 + config: {msg: "Plugin 2"} + - id: "sloth.dev/core/debug/v1" + config: {msg: "Plugin 3"} + sli: + events: + errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) + totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) + alerting: + name: MyServiceHighErrorRate + labels: + category: "availability" + annotations: + summary: "High error rate on 'myservice' requests responses" + pageAlert: + labels: + severity: pageteam + routing_key: myteam + ticketAlert: + labels: + severity: "slack" + slack_channel: "#alerts-myteam" diff --git a/internal/storage/io/k8s_sloth.go b/internal/storage/io/k8s_sloth.go index 008e9676..29fc604e 100644 --- a/internal/storage/io/k8s_sloth.go +++ b/internal/storage/io/k8s_sloth.go @@ -88,7 +88,30 @@ func (l K8sSlothPrometheusYAMLSpecLoader) LoadSpec(ctx context.Context, data []b func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, pluginsRepo SLIPluginRepo, kspec *k8sprometheusv1.PrometheusServiceLevel) (*model.PromSLOGroup, error) { slos := make([]model.PromSLO, 0, len(kspec.Spec.SLOs)) spec := kspec.Spec + + var groupSLOPlugins []model.PromSLOPluginMetadata + if spec.SLOPlugins != nil { + for _, plugin := range spec.SLOPlugins.Chain { + groupSLOPlugins = append(groupSLOPlugins, model.PromSLOPluginMetadata{ + ID: plugin.ID, + Config: plugin.Config, + Priority: plugin.Priority, + }) + } + } + for _, specSLO := range kspec.Spec.SLOs { + plugins := append([]model.PromSLOPluginMetadata{}, groupSLOPlugins...) // Add group plugins if any. + if specSLO.Plugins != nil { + for _, plugin := range specSLO.Plugins.Chain { + plugins = append(plugins, model.PromSLOPluginMetadata{ + ID: plugin.ID, + Config: plugin.Config, + Priority: plugin.Priority, + }) + } + } + slo := model.PromSLO{ ID: fmt.Sprintf("%s-%s", spec.Service, specSLO.Name), Name: specSLO.Name, @@ -99,6 +122,7 @@ func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, plug Labels: utilsdata.MergeLabels(spec.Labels, specSLO.Labels), PageAlertMeta: model.PromAlertMeta{Disable: true}, TicketAlertMeta: model.PromAlertMeta{Disable: true}, + Plugins: model.SLOPlugins{Plugins: plugins}, } // Set SLIs. diff --git a/internal/storage/io/k8s_sloth_test.go b/internal/storage/io/k8s_sloth_test.go index d6864108..2c750a7a 100644 --- a/internal/storage/io/k8s_sloth_test.go +++ b/internal/storage/io/k8s_sloth_test.go @@ -2,6 +2,7 @@ package io_test import ( "context" + "encoding/json" "fmt" "testing" "time" @@ -161,6 +162,7 @@ spec: Objective: 99, PageAlertMeta: model.PromAlertMeta{Disable: true}, TicketAlertMeta: model.PromAlertMeta{Disable: true}, + Plugins: model.SLOPlugins{Plugins: []model.PromSLOPluginMetadata{}}, }, }, OriginalSource: model.PromSLOGroupSource{K8sSlothV1: &kubeslothv1.PrometheusServiceLevel{ @@ -238,6 +240,13 @@ spec: service: "test-svc" labels: owner: "myteam" + sloPlugins: + chain: + - id: test_plugin0 + priority: -100 + config: {"k1": 42} + - id: test_plugin2 + config: {"k1": {"k2": "v2"}} slos: - name: "slo1" labels: @@ -273,6 +282,13 @@ spec: sli: raw: errorRatioQuery: test_expr_ratio_2 + plugins: + chain: + - id: test_plugin1 + priority: 100 + config: + k1: v1 + k2: true alerting: pageAlert: disable: true @@ -323,6 +339,12 @@ spec: "runbook": "http://whatever.com", }, }, + Plugins: model.SLOPlugins{ + Plugins: []model.PromSLOPluginMetadata{ + {ID: "test_plugin0", Priority: -100, Config: json.RawMessage([]byte(`{"k1":42}`))}, + {ID: "test_plugin2", Config: json.RawMessage([]byte(`{"k1":{"k2":"v2"}}`))}, + }, + }, }, { ID: "test-svc-slo2", @@ -341,6 +363,13 @@ spec: }, PageAlertMeta: model.PromAlertMeta{Disable: true}, TicketAlertMeta: model.PromAlertMeta{Disable: true}, + Plugins: model.SLOPlugins{ + Plugins: []model.PromSLOPluginMetadata{ + {ID: "test_plugin0", Priority: -100, Config: json.RawMessage([]byte(`{"k1":42}`))}, + {ID: "test_plugin2", Config: json.RawMessage([]byte(`{"k1":{"k2":"v2"}}`))}, + {ID: "test_plugin1", Priority: 100, Config: json.RawMessage([]byte(`{"k1":"v1","k2":true}`))}, + }, + }, }, }, OriginalSource: model.PromSLOGroupSource{K8sSlothV1: &kubeslothv1.PrometheusServiceLevel{ @@ -354,6 +383,12 @@ spec: Spec: kubeslothv1.PrometheusServiceLevelSpec{ Service: "test-svc", Labels: map[string]string{"owner": "myteam"}, + SLOPlugins: &kubeslothv1.SLOPlugins{ + Chain: []kubeslothv1.SLOPlugin{ + {ID: "test_plugin0", Priority: -100, Config: json.RawMessage([]byte(`{"k1":42}`))}, + {ID: "test_plugin2", Config: json.RawMessage([]byte(`{"k1":{"k2":"v2"}}`))}, + }, + }, SLOs: []kubeslothv1.SLO{ {Name: "slo1", Description: "This is a test.", Objective: 99.99999, Labels: map[string]string{"category": "test"}, SLI: kubeslothv1.SLI{Events: &kubeslothv1.SLIEvents{ @@ -380,6 +415,11 @@ spec: PageAlert: kubeslothv1.Alert{Disable: true}, TicketAlert: kubeslothv1.Alert{Disable: true}, }, + Plugins: &kubeslothv1.SLOPlugins{ + Chain: []kubeslothv1.SLOPlugin{ + {ID: "test_plugin1", Priority: 100, Config: []byte(`{"k1":"v1","k2":true}`)}, + }, + }, }, }, }, diff --git a/internal/storage/io/sloth_test.go b/internal/storage/io/sloth_test.go index 131db1e4..1232ca1a 100644 --- a/internal/storage/io/sloth_test.go +++ b/internal/storage/io/sloth_test.go @@ -257,12 +257,12 @@ service: "test-svc" labels: owner: "myteam" slo_plugins: - chain: - - id: test_plugin0 - priority: -100 - config: {"k1": 42} - - id: test_plugin2 - config: {"k1": {"k2": "v2"}} + chain: + - id: test_plugin0 + priority: -100 + config: {"k1": 42} + - id: test_plugin2 + config: {"k1": {"k2": "v2"}} slos: - name: "slo1" labels: diff --git a/pkg/kubernetes/api/sloth/v1/README.md b/pkg/kubernetes/api/sloth/v1/README.md index 1a509497..eaa650eb 100755 --- a/pkg/kubernetes/api/sloth/v1/README.md +++ b/pkg/kubernetes/api/sloth/v1/README.md @@ -47,6 +47,12 @@ import "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" - [type SLO](<#SLO>) - [func \(in \*SLO\) DeepCopy\(\) \*SLO](<#SLO.DeepCopy>) - [func \(in \*SLO\) DeepCopyInto\(out \*SLO\)](<#SLO.DeepCopyInto>) +- [type SLOPlugin](<#SLOPlugin>) + - [func \(in \*SLOPlugin\) DeepCopy\(\) \*SLOPlugin](<#SLOPlugin.DeepCopy>) + - [func \(in \*SLOPlugin\) DeepCopyInto\(out \*SLOPlugin\)](<#SLOPlugin.DeepCopyInto>) +- [type SLOPlugins](<#SLOPlugins>) + - [func \(in \*SLOPlugins\) DeepCopy\(\) \*SLOPlugins](<#SLOPlugins.DeepCopy>) + - [func \(in \*SLOPlugins\) DeepCopyInto\(out \*SLOPlugins\)](<#SLOPlugins.DeepCopyInto>) ## Variables @@ -96,7 +102,7 @@ func VersionKind(kind string) schema.GroupVersionKind VersionKind takes an unqualified kind and returns back a Group qualified GroupVersionKind. -## type [Alert]() +## type [Alert]() Alert configures specific SLO alert. @@ -118,7 +124,7 @@ type Alert struct { ``` -### func \(\*Alert\) [DeepCopy]() +### func \(\*Alert\) [DeepCopy]() ```go func (in *Alert) DeepCopy() *Alert @@ -127,7 +133,7 @@ func (in *Alert) DeepCopy() *Alert DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Alert. -### func \(\*Alert\) [DeepCopyInto]() +### func \(\*Alert\) [DeepCopyInto]() ```go func (in *Alert) DeepCopyInto(out *Alert) @@ -136,7 +142,7 @@ func (in *Alert) DeepCopyInto(out *Alert) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type [Alerting]() +## type [Alerting]() Alerting wraps all the configuration required by the SLO alerts. @@ -164,7 +170,7 @@ type Alerting struct { ``` -### func \(\*Alerting\) [DeepCopy]() +### func \(\*Alerting\) [DeepCopy]() ```go func (in *Alerting) DeepCopy() *Alerting @@ -173,7 +179,7 @@ func (in *Alerting) DeepCopy() *Alerting DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Alerting. -### func \(\*Alerting\) [DeepCopyInto]() +### func \(\*Alerting\) [DeepCopyInto]() ```go func (in *Alerting) DeepCopyInto(out *Alerting) @@ -182,7 +188,7 @@ func (in *Alerting) DeepCopyInto(out *Alerting) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type [PrometheusServiceLevel]() +## type [PrometheusServiceLevel]() \+genclient \+k8s:deepcopy\-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object \+kubebuilder:subresource:status \+kubebuilder:printcolumn:name="SERVICE",type="string",JSONPath=".spec.service" \+kubebuilder:printcolumn:name="DESIRED SLOs",type="integer",JSONPath=".status.processedSLOs" \+kubebuilder:printcolumn:name="READY SLOs",type="integer",JSONPath=".status.promOpRulesGeneratedSLOs" \+kubebuilder:printcolumn:name="GEN OK",type="boolean",JSONPath=".status.promOpRulesGenerated" \+kubebuilder:printcolumn:name="GEN AGE",type="date",JSONPath=".status.lastPromOpRulesSuccessfulGenerated" \+kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" \+kubebuilder:resource:singular=prometheusservicelevel,path=prometheusservicelevels,shortName=psl;pslo,scope=Namespaced,categories=slo;slos;sli;slis @@ -199,7 +205,7 @@ type PrometheusServiceLevel struct { ``` -### func \(\*PrometheusServiceLevel\) [DeepCopy]() +### func \(\*PrometheusServiceLevel\) [DeepCopy]() ```go func (in *PrometheusServiceLevel) DeepCopy() *PrometheusServiceLevel @@ -208,7 +214,7 @@ func (in *PrometheusServiceLevel) DeepCopy() *PrometheusServiceLevel DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusServiceLevel. -### func \(\*PrometheusServiceLevel\) [DeepCopyInto]() +### func \(\*PrometheusServiceLevel\) [DeepCopyInto]() ```go func (in *PrometheusServiceLevel) DeepCopyInto(out *PrometheusServiceLevel) @@ -217,7 +223,7 @@ func (in *PrometheusServiceLevel) DeepCopyInto(out *PrometheusServiceLevel) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -### func \(\*PrometheusServiceLevel\) [DeepCopyObject]() +### func \(\*PrometheusServiceLevel\) [DeepCopyObject]() ```go func (in *PrometheusServiceLevel) DeepCopyObject() runtime.Object @@ -226,7 +232,7 @@ func (in *PrometheusServiceLevel) DeepCopyObject() runtime.Object DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -## type [PrometheusServiceLevelList]() +## type [PrometheusServiceLevelList]() \+k8s:deepcopy\-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -242,7 +248,7 @@ type PrometheusServiceLevelList struct { ``` -### func \(\*PrometheusServiceLevelList\) [DeepCopy]() +### func \(\*PrometheusServiceLevelList\) [DeepCopy]() ```go func (in *PrometheusServiceLevelList) DeepCopy() *PrometheusServiceLevelList @@ -251,7 +257,7 @@ func (in *PrometheusServiceLevelList) DeepCopy() *PrometheusServiceLevelList DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusServiceLevelList. -### func \(\*PrometheusServiceLevelList\) [DeepCopyInto]() +### func \(\*PrometheusServiceLevelList\) [DeepCopyInto]() ```go func (in *PrometheusServiceLevelList) DeepCopyInto(out *PrometheusServiceLevelList) @@ -260,7 +266,7 @@ func (in *PrometheusServiceLevelList) DeepCopyInto(out *PrometheusServiceLevelLi DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -### func \(\*PrometheusServiceLevelList\) [DeepCopyObject]() +### func \(\*PrometheusServiceLevelList\) [DeepCopyObject]() ```go func (in *PrometheusServiceLevelList) DeepCopyObject() runtime.Object @@ -269,7 +275,7 @@ func (in *PrometheusServiceLevelList) DeepCopyObject() runtime.Object DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -## type [PrometheusServiceLevelSpec]() +## type [PrometheusServiceLevelSpec]() ServiceLevelSpec is the spec for a PrometheusServiceLevel. @@ -284,6 +290,10 @@ type PrometheusServiceLevelSpec struct { // and alerting rules generated for the service SLOs. Labels map[string]string `json:"labels,omitempty"` + // SLOPlugins will be added to the SLO generation plugin chain of all SLOs. + // +optional + SLOPlugins *SLOPlugins `json:"sloPlugins,omitempty"` + // +kubebuilder:validation:MinItems=1 // // SLOs are the SLOs of the service. @@ -292,7 +302,7 @@ type PrometheusServiceLevelSpec struct { ``` -### func \(\*PrometheusServiceLevelSpec\) [DeepCopy]() +### func \(\*PrometheusServiceLevelSpec\) [DeepCopy]() ```go func (in *PrometheusServiceLevelSpec) DeepCopy() *PrometheusServiceLevelSpec @@ -301,7 +311,7 @@ func (in *PrometheusServiceLevelSpec) DeepCopy() *PrometheusServiceLevelSpec DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusServiceLevelSpec. -### func \(\*PrometheusServiceLevelSpec\) [DeepCopyInto]() +### func \(\*PrometheusServiceLevelSpec\) [DeepCopyInto]() ```go func (in *PrometheusServiceLevelSpec) DeepCopyInto(out *PrometheusServiceLevelSpec) @@ -310,7 +320,7 @@ func (in *PrometheusServiceLevelSpec) DeepCopyInto(out *PrometheusServiceLevelSp DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type [PrometheusServiceLevelStatus]() +## type [PrometheusServiceLevelStatus]() @@ -333,7 +343,7 @@ type PrometheusServiceLevelStatus struct { ``` -### func \(\*PrometheusServiceLevelStatus\) [DeepCopy]() +### func \(\*PrometheusServiceLevelStatus\) [DeepCopy]() ```go func (in *PrometheusServiceLevelStatus) DeepCopy() *PrometheusServiceLevelStatus @@ -342,7 +352,7 @@ func (in *PrometheusServiceLevelStatus) DeepCopy() *PrometheusServiceLevelStatus DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusServiceLevelStatus. -### func \(\*PrometheusServiceLevelStatus\) [DeepCopyInto]() +### func \(\*PrometheusServiceLevelStatus\) [DeepCopyInto]() ```go func (in *PrometheusServiceLevelStatus) DeepCopyInto(out *PrometheusServiceLevelStatus) @@ -351,7 +361,7 @@ func (in *PrometheusServiceLevelStatus) DeepCopyInto(out *PrometheusServiceLevel DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type [SLI]() +## type [SLI]() SLI will tell what is good or bad for the SLO. All SLIs will be get based on time windows, that's why Sloth needs the queries to use \`\{\{.window\}\}\` template variable. @@ -374,7 +384,7 @@ type SLI struct { ``` -### func \(\*SLI\) [DeepCopy]() +### func \(\*SLI\) [DeepCopy]() ```go func (in *SLI) DeepCopy() *SLI @@ -383,7 +393,7 @@ func (in *SLI) DeepCopy() *SLI DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLI. -### func \(\*SLI\) [DeepCopyInto]() +### func \(\*SLI\) [DeepCopyInto]() ```go func (in *SLI) DeepCopyInto(out *SLI) @@ -392,7 +402,7 @@ func (in *SLI) DeepCopyInto(out *SLI) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type [SLIEvents]() +## type [SLIEvents]() SLIEvents is an SLI that is calculated as the division of bad events and total events, giving a ratio SLI. Normally this is the most common ratio type. @@ -411,7 +421,7 @@ type SLIEvents struct { ``` -### func \(\*SLIEvents\) [DeepCopy]() +### func \(\*SLIEvents\) [DeepCopy]() ```go func (in *SLIEvents) DeepCopy() *SLIEvents @@ -420,7 +430,7 @@ func (in *SLIEvents) DeepCopy() *SLIEvents DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLIEvents. -### func \(\*SLIEvents\) [DeepCopyInto]() +### func \(\*SLIEvents\) [DeepCopyInto]() ```go func (in *SLIEvents) DeepCopyInto(out *SLIEvents) @@ -429,7 +439,7 @@ func (in *SLIEvents) DeepCopyInto(out *SLIEvents) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type [SLIPlugin]() +## type [SLIPlugin]() SLIPlugin will use the SLI returned by the SLI plugin selected along with the options. @@ -445,7 +455,7 @@ type SLIPlugin struct { ``` -### func \(\*SLIPlugin\) [DeepCopy]() +### func \(\*SLIPlugin\) [DeepCopy]() ```go func (in *SLIPlugin) DeepCopy() *SLIPlugin @@ -454,7 +464,7 @@ func (in *SLIPlugin) DeepCopy() *SLIPlugin DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLIPlugin. -### func \(\*SLIPlugin\) [DeepCopyInto]() +### func \(\*SLIPlugin\) [DeepCopyInto]() ```go func (in *SLIPlugin) DeepCopyInto(out *SLIPlugin) @@ -463,7 +473,7 @@ func (in *SLIPlugin) DeepCopyInto(out *SLIPlugin) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type [SLIRaw]() +## type [SLIRaw]() SLIRaw is a error ratio SLI already calculated. Normally this will be used when the SLI is already calculated by other recording rule, system... @@ -475,7 +485,7 @@ type SLIRaw struct { ``` -### func \(\*SLIRaw\) [DeepCopy]() +### func \(\*SLIRaw\) [DeepCopy]() ```go func (in *SLIRaw) DeepCopy() *SLIRaw @@ -484,7 +494,7 @@ func (in *SLIRaw) DeepCopy() *SLIRaw DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLIRaw. -### func \(\*SLIRaw\) [DeepCopyInto]() +### func \(\*SLIRaw\) [DeepCopyInto]() ```go func (in *SLIRaw) DeepCopyInto(out *SLIRaw) @@ -493,7 +503,7 @@ func (in *SLIRaw) DeepCopyInto(out *SLIRaw) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type [SLO]() +## type [SLO]() SLO is the configuration/declaration of the service level objective of a service. @@ -514,6 +524,11 @@ type SLO struct { // Objective is target of the SLO the percentage (0, 100] (e.g 99.9). Objective float64 `json:"objective"` + // Plugins will be added along the group SLO plugins declared in the spec root level + // and Sloth default plugins. + // +optional + Plugins *SLOPlugins `json:"plugins,omitempty"` + // Labels are the Prometheus labels that will have all the recording and // alerting rules for this specific SLO. These labels are merged with the // previous level labels. @@ -534,7 +549,7 @@ type SLO struct { ``` -### func \(\*SLO\) [DeepCopy]() +### func \(\*SLO\) [DeepCopy]() ```go func (in *SLO) DeepCopy() *SLO @@ -543,7 +558,7 @@ func (in *SLO) DeepCopy() *SLO DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLO. -### func \(\*SLO\) [DeepCopyInto]() +### func \(\*SLO\) [DeepCopyInto]() ```go func (in *SLO) DeepCopyInto(out *SLO) @@ -551,4 +566,81 @@ func (in *SLO) DeepCopyInto(out *SLO) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. + +## type [SLOPlugin]() + +SLOPlugin is a plugin that will be used on the chain of plugins for the SLO generation. + +```go +type SLOPlugin struct { + // ID is the ID of the plugin to load . + ID string `json:"id"` + + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Type=object + // + // Config is the configuration used on the plugin instance creation. + // +optional + Config json.RawMessage `json:"config,omitempty"` + + // Priority is the priority of the plugin in the chain. The lower the number + // the higher the priority. The first plugin will be the one with the lowest + // priority. + // The default plugins loaded by Sloth use `0` priority. If you want to + // execute plugins before the default ones, you can use negative priority. + // It is recommended to use round gaps of numbers like 10, 100, 1000, -200, -1000... + // +optional + Priority int `json:"priority,omitempty"` +} +``` + + +### func \(\*SLOPlugin\) [DeepCopy]() + +```go +func (in *SLOPlugin) DeepCopy() *SLOPlugin +``` + +DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLOPlugin. + + +### func \(\*SLOPlugin\) [DeepCopyInto]() + +```go +func (in *SLOPlugin) DeepCopyInto(out *SLOPlugin) +``` + +DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. + + +## type [SLOPlugins]() + +SLOPlugins are the list plugins that will be used on the process of SLOs for the rules generation. + +```go +type SLOPlugins struct { + // chain ths the list of plugin chain to add to the SLO generation. + Chain []SLOPlugin `json:"chain"` +} +``` + + +### func \(\*SLOPlugins\) [DeepCopy]() + +```go +func (in *SLOPlugins) DeepCopy() *SLOPlugins +``` + +DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLOPlugins. + + +### func \(\*SLOPlugins\) [DeepCopyInto]() + +```go +func (in *SLOPlugins) DeepCopyInto(out *SLOPlugins) +``` + +DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. + Generated by [gomarkdoc]() diff --git a/pkg/kubernetes/api/sloth/v1/types.go b/pkg/kubernetes/api/sloth/v1/types.go index 00e27c73..bdc25165 100644 --- a/pkg/kubernetes/api/sloth/v1/types.go +++ b/pkg/kubernetes/api/sloth/v1/types.go @@ -1,6 +1,8 @@ package v1 import ( + "encoding/json" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -38,6 +40,10 @@ type PrometheusServiceLevelSpec struct { // and alerting rules generated for the service SLOs. Labels map[string]string `json:"labels,omitempty"` + // SLOPlugins will be added to the SLO generation plugin chain of all SLOs. + // +optional + SLOPlugins *SLOPlugins `json:"sloPlugins,omitempty"` + // +kubebuilder:validation:MinItems=1 // // SLOs are the SLOs of the service. @@ -62,6 +68,11 @@ type SLO struct { // Objective is target of the SLO the percentage (0, 100] (e.g 99.9). Objective float64 `json:"objective"` + // Plugins will be added along the group SLO plugins declared in the spec root level + // and Sloth default plugins. + // +optional + Plugins *SLOPlugins `json:"plugins,omitempty"` + // Labels are the Prometheus labels that will have all the recording and // alerting rules for this specific SLO. These labels are merged with the // previous level labels. @@ -168,6 +179,36 @@ type Alert struct { Annotations map[string]string `json:"annotations,omitempty"` } +// SLOPlugins are the list plugins that will be used on the process of SLOs for the +// rules generation. +type SLOPlugins struct { + // chain ths the list of plugin chain to add to the SLO generation. + Chain []SLOPlugin `json:"chain"` +} + +// SLOPlugin is a plugin that will be used on the chain of plugins for the SLO generation. +type SLOPlugin struct { + // ID is the ID of the plugin to load . + ID string `json:"id"` + + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Type=object + // + // Config is the configuration used on the plugin instance creation. + // +optional + Config json.RawMessage `json:"config,omitempty"` + + // Priority is the priority of the plugin in the chain. The lower the number + // the higher the priority. The first plugin will be the one with the lowest + // priority. + // The default plugins loaded by Sloth use `0` priority. If you want to + // execute plugins before the default ones, you can use negative priority. + // It is recommended to use round gaps of numbers like 10, 100, 1000, -200, -1000... + // +optional + Priority int `json:"priority,omitempty"` +} + type PrometheusServiceLevelStatus struct { // PromOpRulesGeneratedSLOs tells how many SLOs have been processed and generated for Prometheus operator successfully. PromOpRulesGeneratedSLOs int `json:"promOpRulesGeneratedSLOs"` diff --git a/pkg/kubernetes/api/sloth/v1/zz_generated.deepcopy.go b/pkg/kubernetes/api/sloth/v1/zz_generated.deepcopy.go index ea5cdfb9..d14d094c 100644 --- a/pkg/kubernetes/api/sloth/v1/zz_generated.deepcopy.go +++ b/pkg/kubernetes/api/sloth/v1/zz_generated.deepcopy.go @@ -6,6 +6,8 @@ package v1 import ( + json "encoding/json" + runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -142,6 +144,11 @@ func (in *PrometheusServiceLevelSpec) DeepCopyInto(out *PrometheusServiceLevelSp (*out)[key] = val } } + if in.SLOPlugins != nil { + in, out := &in.SLOPlugins, &out.SLOPlugins + *out = new(SLOPlugins) + (*in).DeepCopyInto(*out) + } if in.SLOs != nil { in, out := &in.SLOs, &out.SLOs *out = make([]SLO, len(*in)) @@ -271,6 +278,11 @@ func (in *SLIRaw) DeepCopy() *SLIRaw { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SLO) DeepCopyInto(out *SLO) { *out = *in + if in.Plugins != nil { + in, out := &in.Plugins, &out.Plugins + *out = new(SLOPlugins) + (*in).DeepCopyInto(*out) + } if in.Labels != nil { in, out := &in.Labels, &out.Labels *out = make(map[string]string, len(*in)) @@ -292,3 +304,47 @@ func (in *SLO) DeepCopy() *SLO { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SLOPlugin) DeepCopyInto(out *SLOPlugin) { + *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make(json.RawMessage, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLOPlugin. +func (in *SLOPlugin) DeepCopy() *SLOPlugin { + if in == nil { + return nil + } + out := new(SLOPlugin) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SLOPlugins) DeepCopyInto(out *SLOPlugins) { + *out = *in + if in.Chain != nil { + in, out := &in.Chain, &out.Chain + *out = make([]SLOPlugin, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SLOPlugins. +func (in *SLOPlugins) DeepCopy() *SLOPlugins { + if in == nil { + return nil + } + out := new(SLOPlugins) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelspec.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelspec.go index 8f5706a7..f4b94e4a 100644 --- a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelspec.go +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevelspec.go @@ -5,9 +5,10 @@ package v1 // PrometheusServiceLevelSpecApplyConfiguration represents a declarative configuration of the PrometheusServiceLevelSpec type for use // with apply. type PrometheusServiceLevelSpecApplyConfiguration struct { - Service *string `json:"service,omitempty"` - Labels map[string]string `json:"labels,omitempty"` - SLOs []SLOApplyConfiguration `json:"slos,omitempty"` + Service *string `json:"service,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + SLOPlugins *SLOPluginsApplyConfiguration `json:"sloPlugins,omitempty"` + SLOs []SLOApplyConfiguration `json:"slos,omitempty"` } // PrometheusServiceLevelSpecApplyConfiguration constructs a declarative configuration of the PrometheusServiceLevelSpec type for use with @@ -38,6 +39,14 @@ func (b *PrometheusServiceLevelSpecApplyConfiguration) WithLabels(entries map[st return b } +// WithSLOPlugins sets the SLOPlugins field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the SLOPlugins field is set to the value of the last call. +func (b *PrometheusServiceLevelSpecApplyConfiguration) WithSLOPlugins(value *SLOPluginsApplyConfiguration) *PrometheusServiceLevelSpecApplyConfiguration { + b.SLOPlugins = value + return b +} + // WithSLOs adds the given value to the SLOs field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the SLOs field. diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/slo.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/slo.go index fe9ee74f..cf19e96e 100644 --- a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/slo.go +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/slo.go @@ -5,12 +5,13 @@ package v1 // SLOApplyConfiguration represents a declarative configuration of the SLO type for use // with apply. type SLOApplyConfiguration struct { - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - Objective *float64 `json:"objective,omitempty"` - Labels map[string]string `json:"labels,omitempty"` - SLI *SLIApplyConfiguration `json:"sli,omitempty"` - Alerting *AlertingApplyConfiguration `json:"alerting,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Objective *float64 `json:"objective,omitempty"` + Plugins *SLOPluginsApplyConfiguration `json:"plugins,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + SLI *SLIApplyConfiguration `json:"sli,omitempty"` + Alerting *AlertingApplyConfiguration `json:"alerting,omitempty"` } // SLOApplyConfiguration constructs a declarative configuration of the SLO type for use with @@ -43,6 +44,14 @@ func (b *SLOApplyConfiguration) WithObjective(value float64) *SLOApplyConfigurat return b } +// WithPlugins sets the Plugins field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Plugins field is set to the value of the last call. +func (b *SLOApplyConfiguration) WithPlugins(value *SLOPluginsApplyConfiguration) *SLOApplyConfiguration { + b.Plugins = value + return b +} + // WithLabels puts the entries into the Labels field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, the entries provided by each call will be put on the Labels field, diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugin.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugin.go new file mode 100644 index 00000000..c3f8c64b --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugin.go @@ -0,0 +1,45 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +import ( + json "encoding/json" +) + +// SLOPluginApplyConfiguration represents a declarative configuration of the SLOPlugin type for use +// with apply. +type SLOPluginApplyConfiguration struct { + ID *string `json:"id,omitempty"` + Config *json.RawMessage `json:"config,omitempty"` + Priority *int `json:"priority,omitempty"` +} + +// SLOPluginApplyConfiguration constructs a declarative configuration of the SLOPlugin type for use with +// apply. +func SLOPlugin() *SLOPluginApplyConfiguration { + return &SLOPluginApplyConfiguration{} +} + +// WithID sets the ID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ID field is set to the value of the last call. +func (b *SLOPluginApplyConfiguration) WithID(value string) *SLOPluginApplyConfiguration { + b.ID = &value + return b +} + +// WithConfig sets the Config field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Config field is set to the value of the last call. +func (b *SLOPluginApplyConfiguration) WithConfig(value json.RawMessage) *SLOPluginApplyConfiguration { + b.Config = &value + return b +} + +// WithPriority sets the Priority field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Priority field is set to the value of the last call. +func (b *SLOPluginApplyConfiguration) WithPriority(value int) *SLOPluginApplyConfiguration { + b.Priority = &value + return b +} diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugins.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugins.go new file mode 100644 index 00000000..5bc0e656 --- /dev/null +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugins.go @@ -0,0 +1,28 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +// SLOPluginsApplyConfiguration represents a declarative configuration of the SLOPlugins type for use +// with apply. +type SLOPluginsApplyConfiguration struct { + Chain []SLOPluginApplyConfiguration `json:"chain,omitempty"` +} + +// SLOPluginsApplyConfiguration constructs a declarative configuration of the SLOPlugins type for use with +// apply. +func SLOPlugins() *SLOPluginsApplyConfiguration { + return &SLOPluginsApplyConfiguration{} +} + +// WithChain adds the given value to the Chain field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Chain field. +func (b *SLOPluginsApplyConfiguration) WithChain(values ...*SLOPluginApplyConfiguration) *SLOPluginsApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithChain") + } + b.Chain = append(b.Chain, *values[i]) + } + return b +} diff --git a/pkg/kubernetes/gen/applyconfiguration/utils.go b/pkg/kubernetes/gen/applyconfiguration/utils.go index 1962cfae..314db108 100644 --- a/pkg/kubernetes/gen/applyconfiguration/utils.go +++ b/pkg/kubernetes/gen/applyconfiguration/utils.go @@ -36,6 +36,10 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &slothv1.SLIRawApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("SLO"): return &slothv1.SLOApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("SLOPlugin"): + return &slothv1.SLOPluginApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("SLOPlugins"): + return &slothv1.SLOPluginsApplyConfiguration{} } return nil diff --git a/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml b/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml index 83cd4c98..ffdf7964 100644 --- a/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml +++ b/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml @@ -78,6 +78,41 @@ spec: service: description: Service is the application of the SLOs. type: string + sloPlugins: + description: SLOPlugins will be added to the SLO generation plugin + chain of all SLOs. + properties: + chain: + description: chain ths the list of plugin chain to add to the + SLO generation. + items: + description: SLOPlugin is a plugin that will be used on the + chain of plugins for the SLO generation. + properties: + config: + description: Config is the configuration used on the plugin + instance creation. + type: object + x-kubernetes-preserve-unknown-fields: true + id: + description: ID is the ID of the plugin to load . + type: string + priority: + description: |- + Priority is the priority of the plugin in the chain. The lower the number + the higher the priority. The first plugin will be the one with the lowest + priority. + The default plugins loaded by Sloth use `0` priority. If you want to + execute plugins before the default ones, you can use negative priority. + It is recommended to use round gaps of numbers like 10, 100, 1000, -200, -1000... + type: integer + required: + - id + type: object + type: array + required: + - chain + type: object slos: description: SLOs are the SLOs of the service. items: @@ -173,6 +208,42 @@ spec: description: Objective is target of the SLO the percentage (0, 100] (e.g 99.9). type: number + plugins: + description: |- + Plugins will be added along the group SLO plugins declared in the spec root level + and Sloth default plugins. + properties: + chain: + description: chain ths the list of plugin chain to add to + the SLO generation. + items: + description: SLOPlugin is a plugin that will be used on + the chain of plugins for the SLO generation. + properties: + config: + description: Config is the configuration used on the + plugin instance creation. + type: object + x-kubernetes-preserve-unknown-fields: true + id: + description: ID is the ID of the plugin to load . + type: string + priority: + description: |- + Priority is the priority of the plugin in the chain. The lower the number + the higher the priority. The first plugin will be the one with the lowest + priority. + The default plugins loaded by Sloth use `0` priority. If you want to + execute plugins before the default ones, you can use negative priority. + It is recommended to use round gaps of numbers like 10, 100, 1000, -200, -1000... + type: integer + required: + - id + type: object + type: array + required: + - chain + type: object sli: description: SLI is the indicator (service level indicator) for this specific SLO. diff --git a/test/integration/k8scontroller/exp_plugin_test.go b/test/integration/k8scontroller/exp_sli_plugins_test.go similarity index 98% rename from test/integration/k8scontroller/exp_plugin_test.go rename to test/integration/k8scontroller/exp_sli_plugins_test.go index 01f14568..3019859a 100644 --- a/test/integration/k8scontroller/exp_plugin_test.go +++ b/test/integration/k8scontroller/exp_sli_plugins_test.go @@ -8,7 +8,7 @@ import ( slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" ) -func getPluginPrometheusServiceLevel() *slothv1.PrometheusServiceLevel { +func getSLIPluginsPrometheusServiceLevel() *slothv1.PrometheusServiceLevel { return &slothv1.PrometheusServiceLevel{ ObjectMeta: metav1.ObjectMeta{ Name: "test01", @@ -47,7 +47,7 @@ func getPluginPrometheusServiceLevel() *slothv1.PrometheusServiceLevel { } } -func getPluginPromOpPrometheusRule(slothVersion string) *monitoringv1.PrometheusRule { +func getSLIPluginsPromOpPrometheusRule(slothVersion string) *monitoringv1.PrometheusRule { return &monitoringv1.PrometheusRule{ ObjectMeta: metav1.ObjectMeta{ Name: "test01", diff --git a/test/integration/k8scontroller/exp_slo_plugins_test.go b/test/integration/k8scontroller/exp_slo_plugins_test.go new file mode 100644 index 00000000..ad1f3469 --- /dev/null +++ b/test/integration/k8scontroller/exp_slo_plugins_test.go @@ -0,0 +1,497 @@ +package k8scontroller_test + +import ( + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" +) + +func getSLOPluginsPrometheusServiceLevel() *slothv1.PrometheusServiceLevel { + return &slothv1.PrometheusServiceLevel{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test01", + Labels: map[string]string{ + "prometheus": "default", + }, + }, + Spec: slothv1.PrometheusServiceLevelSpec{ + Service: "svc01", + Labels: map[string]string{ + "globalk1": "globalv1", + }, + SLOPlugins: &slothv1.SLOPlugins{ + Chain: []slothv1.SLOPlugin{ + {ID: "integration-tests/plugin1", Priority: 9999999, Config: []byte(`{"labels":{"k1":"v1","k2":"v2"}}`)}, + {ID: "integration-tests/plugin1", Priority: -999999, Config: []byte(`{"labels":{"k3":"v3"}}`)}, // These should be replaced because is before defaults. + }, + }, + SLOs: []slothv1.SLO{ + { + Name: "slo01", + Objective: 99.9, + Labels: map[string]string{ + "slo01k1": "slo01v1", + }, + SLI: slothv1.SLI{Events: &slothv1.SLIEvents{ + ErrorQuery: `sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}]))`, + TotalQuery: `sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}]))`, + }}, + Alerting: slothv1.Alerting{ + Name: "myServiceAlert", + Labels: map[string]string{ + "alert01k1": "alert01v1", + }, + Annotations: map[string]string{ + "alert02k1": "alert02v1", + }, + PageAlert: slothv1.Alert{}, + TicketAlert: slothv1.Alert{}, + }, + }, + { + Name: "slo02", + Objective: 99.99, + SLI: slothv1.SLI{Raw: &slothv1.SLIRaw{ + ErrorRatioQuery: `sum(rate(http_request_duration_seconds_count{job="myservice2",code=~"(5..|429)"}[{{.window}}])) +/ +sum(rate(http_request_duration_seconds_count{job="myservice2"}[{{.window}}])) +`, + }}, + Alerting: slothv1.Alerting{ + PageAlert: slothv1.Alert{Disable: true}, + TicketAlert: slothv1.Alert{Disable: true}, + }, + Plugins: &slothv1.SLOPlugins{ + Chain: []slothv1.SLOPlugin{ + {ID: "integration-tests/plugin1", Config: []byte(`{"labels":{"k4":"v4"}}`)}, + {ID: "integration-tests/plugin1", Priority: 1000, Config: []byte(`{"labels":{"k2":"v0","k5":"v5"}}`)}, // k2 should be replaced by a (9999999 priority) plugin. + }, + }, + }, + }, + }, + } +} + +func getSLOPluginsPromOpPrometheusRule(slothVersion string) *monitoringv1.PrometheusRule { + return &monitoringv1.PrometheusRule{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test01", + Labels: map[string]string{ + "prometheus": "default", + "app.kubernetes.io/component": "SLO", + "app.kubernetes.io/managed-by": "sloth", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "sloth.slok.dev/v1", + Kind: "PrometheusServiceLevel", + Name: "test01", + }, + }, + }, + Spec: monitoringv1.PrometheusRuleSpec{ + Groups: []monitoringv1.RuleGroup{ + { + Name: "sloth-slo-sli-recordings-svc01-slo01", + Rules: []monitoringv1.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[5m])))\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[5m])))\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + "sloth_window": "5m", + }, + }, + { + Record: "slo:sli_error:ratio_rate30m", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[30m])))\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[30m])))\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + "sloth_window": "30m", + }, + }, + { + Record: "slo:sli_error:ratio_rate1h", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[1h])))\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[1h])))\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + "sloth_window": "1h", + }, + }, + { + Record: "slo:sli_error:ratio_rate2h", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[2h])))\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[2h])))\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + "sloth_window": "2h", + }, + }, + { + Record: "slo:sli_error:ratio_rate6h", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[6h])))\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[6h])))\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + "sloth_window": "6h", + }, + }, + { + Record: "slo:sli_error:ratio_rate1d", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[1d])))\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[1d])))\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + "sloth_window": "1d", + }, + }, + { + Record: "slo:sli_error:ratio_rate3d", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[3d])))\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[3d])))\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + "sloth_window": "3d", + }, + }, + { + Record: "slo:sli_error:ratio_rate30d", + Expr: intstr.FromString("sum_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"}[30d])\n/ ignoring (sloth_window)\ncount_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"}[30d])\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + "sloth_window": "30d", + }, + }, + }, + }, + { + Name: "sloth-slo-meta-recordings-svc01-slo01", + Rules: []monitoringv1.Rule{ + { + Record: "slo:objective:ratio", + Expr: intstr.FromString("vector(0.9990000000000001)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + }, + }, + { + Record: "slo:error_budget:ratio", + Expr: intstr.FromString("vector(1-0.9990000000000001)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + }, + }, + { + Record: "slo:time_period:days", + Expr: intstr.FromString("vector(30)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + }, + }, + { + Record: "slo:current_burn_rate:ratio", + Expr: intstr.FromString("slo:sli_error:ratio_rate5m{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"}\n/ on(sloth_id, sloth_slo, sloth_service) group_left\nslo:error_budget:ratio{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"}\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + }, + }, + { + Record: "slo:period_burn_rate:ratio", + Expr: intstr.FromString("slo:sli_error:ratio_rate30d{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"}\n/ on(sloth_id, sloth_slo, sloth_service) group_left\nslo:error_budget:ratio{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"}\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + }, + }, + { + Record: "slo:period_error_budget_remaining:ratio", + Expr: intstr.FromString("1 - slo:period_burn_rate:ratio{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"}"), + Labels: map[string]string{ + "globalk1": "globalv1", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + }, + }, + { + Record: "sloth_slo_info", + Expr: intstr.FromString("vector(1)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "k1": "v1", + "k2": "v2", + "slo01k1": "slo01v1", + "sloth_id": "svc01-slo01", + "sloth_service": "svc01", + "sloth_slo": "slo01", + "sloth_mode": "ctrl-gen-k8s", + "sloth_spec": "sloth.slok.dev/v1", + "sloth_version": slothVersion, + "sloth_objective": "99.9", + }, + }, + }, + }, + + { + Name: "sloth-slo-alerts-svc01-slo01", + Rules: []monitoringv1.Rule{ + { + Alert: "myServiceAlert", + Expr: intstr.FromString("(\n max(slo:sli_error:ratio_rate5m{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"} > (14.4 * 0.0009999999999999432)) without (sloth_window)\n and\n max(slo:sli_error:ratio_rate1h{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"} > (14.4 * 0.0009999999999999432)) without (sloth_window)\n)\nor\n(\n max(slo:sli_error:ratio_rate30m{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"} > (6 * 0.0009999999999999432)) without (sloth_window)\n and\n max(slo:sli_error:ratio_rate6h{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"} > (6 * 0.0009999999999999432)) without (sloth_window)\n)\n"), + Labels: map[string]string{ + "alert01k1": "alert01v1", + "sloth_severity": "page", + }, + Annotations: map[string]string{ + "alert02k1": "alert02v1", + "summary": "{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is over expected.", + "title": "(page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast.", + }, + }, + { + Alert: "myServiceAlert", + Expr: intstr.FromString("(\n max(slo:sli_error:ratio_rate2h{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"} > (3 * 0.0009999999999999432)) without (sloth_window)\n and\n max(slo:sli_error:ratio_rate1d{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"} > (3 * 0.0009999999999999432)) without (sloth_window)\n)\nor\n(\n max(slo:sli_error:ratio_rate6h{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"} > (1 * 0.0009999999999999432)) without (sloth_window)\n and\n max(slo:sli_error:ratio_rate3d{sloth_id=\"svc01-slo01\", sloth_service=\"svc01\", sloth_slo=\"slo01\"} > (1 * 0.0009999999999999432)) without (sloth_window)\n)\n"), + Labels: map[string]string{ + "alert01k1": "alert01v1", + "sloth_severity": "ticket", + }, + Annotations: map[string]string{ + "alert02k1": "alert02v1", + "summary": "{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is over expected.", + "title": "(ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast.", + }, + }, + }, + }, + { + Name: "sloth-slo-sli-recordings-svc01-slo02", + Rules: []monitoringv1.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice2\",code=~\"(5..|429)\"}[5m]))\n/\nsum(rate(http_request_duration_seconds_count{job=\"myservice2\"}[5m]))\n)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + "sloth_window": "5m", + }, + }, + { + Record: "slo:sli_error:ratio_rate30m", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice2\",code=~\"(5..|429)\"}[30m]))\n/\nsum(rate(http_request_duration_seconds_count{job=\"myservice2\"}[30m]))\n)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + "sloth_window": "30m", + }, + }, + { + Record: "slo:sli_error:ratio_rate1h", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice2\",code=~\"(5..|429)\"}[1h]))\n/\nsum(rate(http_request_duration_seconds_count{job=\"myservice2\"}[1h]))\n)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + "sloth_window": "1h", + }, + }, + { + Record: "slo:sli_error:ratio_rate2h", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice2\",code=~\"(5..|429)\"}[2h]))\n/\nsum(rate(http_request_duration_seconds_count{job=\"myservice2\"}[2h]))\n)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + "sloth_window": "2h", + }, + }, + { + Record: "slo:sli_error:ratio_rate6h", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice2\",code=~\"(5..|429)\"}[6h]))\n/\nsum(rate(http_request_duration_seconds_count{job=\"myservice2\"}[6h]))\n)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + "sloth_window": "6h", + }, + }, + { + Record: "slo:sli_error:ratio_rate1d", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice2\",code=~\"(5..|429)\"}[1d]))\n/\nsum(rate(http_request_duration_seconds_count{job=\"myservice2\"}[1d]))\n)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + "sloth_window": "1d", + }, + }, + { + Record: "slo:sli_error:ratio_rate3d", + Expr: intstr.FromString("(sum(rate(http_request_duration_seconds_count{job=\"myservice2\",code=~\"(5..|429)\"}[3d]))\n/\nsum(rate(http_request_duration_seconds_count{job=\"myservice2\"}[3d]))\n)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + "sloth_window": "3d", + }, + }, + { + Record: "slo:sli_error:ratio_rate30d", + Expr: intstr.FromString("sum_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"svc01-slo02\", sloth_service=\"svc01\", sloth_slo=\"slo02\"}[30d])\n/ ignoring (sloth_window)\ncount_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"svc01-slo02\", sloth_service=\"svc01\", sloth_slo=\"slo02\"}[30d])\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + "sloth_window": "30d", + }, + }, + }, + }, + { + Name: "sloth-slo-meta-recordings-svc01-slo02", + Rules: []monitoringv1.Rule{ + { + Record: "slo:objective:ratio", + Expr: intstr.FromString("vector(0.9998999999999999)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + }, + }, + { + Record: "slo:error_budget:ratio", + Expr: intstr.FromString("vector(1-0.9998999999999999)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + }, + }, + { + Record: "slo:time_period:days", + Expr: intstr.FromString("vector(30)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + }, + }, + { + Record: "slo:current_burn_rate:ratio", + Expr: intstr.FromString("slo:sli_error:ratio_rate5m{sloth_id=\"svc01-slo02\", sloth_service=\"svc01\", sloth_slo=\"slo02\"}\n/ on(sloth_id, sloth_slo, sloth_service) group_left\nslo:error_budget:ratio{sloth_id=\"svc01-slo02\", sloth_service=\"svc01\", sloth_slo=\"slo02\"}\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + }, + }, + { + Record: "slo:period_burn_rate:ratio", + Expr: intstr.FromString("slo:sli_error:ratio_rate30d{sloth_id=\"svc01-slo02\", sloth_service=\"svc01\", sloth_slo=\"slo02\"}\n/ on(sloth_id, sloth_slo, sloth_service) group_left\nslo:error_budget:ratio{sloth_id=\"svc01-slo02\", sloth_service=\"svc01\", sloth_slo=\"slo02\"}\n"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + }, + }, + { + Record: "slo:period_error_budget_remaining:ratio", + Expr: intstr.FromString("1 - slo:period_burn_rate:ratio{sloth_id=\"svc01-slo02\", sloth_service=\"svc01\", sloth_slo=\"slo02\"}"), + Labels: map[string]string{ + "globalk1": "globalv1", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + }, + }, + { + Record: "sloth_slo_info", + Expr: intstr.FromString("vector(1)"), + Labels: map[string]string{ + "globalk1": "globalv1", + "k1": "v1", + "k2": "v2", + "k4": "v4", + "k5": "v5", + "sloth_id": "svc01-slo02", + "sloth_service": "svc01", + "sloth_slo": "slo02", + "sloth_mode": "ctrl-gen-k8s", + "sloth_spec": "sloth.slok.dev/v1", + "sloth_version": slothVersion, + "sloth_objective": "99.99", + }, + }, + }, + }, + }, + }, + } +} diff --git a/test/integration/k8scontroller/k8scontroller_test.go b/test/integration/k8scontroller/k8scontroller_test.go index 0fe15e1d..59a338a2 100644 --- a/test/integration/k8scontroller/k8scontroller_test.go +++ b/test/integration/k8scontroller/k8scontroller_test.go @@ -97,11 +97,11 @@ func TestKubernetesControllerPromOperatorGenerate(t *testing.T) { }, }, - "Having SLOs with plugins as a CRD should generate Prometheus operator CRD.": { + "Having SLOs with SLI plugins as a CRD should generate Prometheus operator CRD.": { sloPeriod: "30d", exec: func(ctx context.Context, t *testing.T, ns string, kubeClis *k8scontroller.KubeClients) { - // Prepare our SLO on Kubernetes with plugin based SLO. - SLOs := getPluginPrometheusServiceLevel() + // Prepare our SLO on Kubernetes with SLI plugin based SLO. + SLOs := getSLIPluginsPrometheusServiceLevel() _, err := kubeClis.Sloth.SlothV1().PrometheusServiceLevels(ns).Create(ctx, SLOs, metav1.CreateOptions{}) require.NoError(t, err) @@ -109,7 +109,30 @@ func TestKubernetesControllerPromOperatorGenerate(t *testing.T) { time.Sleep(500 * time.Millisecond) // Check. - expRule := getPluginPromOpPrometheusRule(version) + expRule := getSLIPluginsPromOpPrometheusRule(version) + expRule.Namespace = ns + + gotRule, err := kubeClis.Monitoring.MonitoringV1().PrometheusRules(ns).Get(ctx, expRule.Name, metav1.GetOptions{}) + gotRule = sanitizePrometheusRule(gotRule) // Remove variations. + require.NoError(t, err) + + assert.Equal(t, expRule, gotRule) + }, + }, + + "Having SLOs with SLO plugins as a CRD should generate Prometheus operator CRD.": { + sloPeriod: "30d", + exec: func(ctx context.Context, t *testing.T, ns string, kubeClis *k8scontroller.KubeClients) { + // Prepare our SLO on Kubernetes with SLO plugin based SLO. + SLOs := getSLOPluginsPrometheusServiceLevel() + _, err := kubeClis.Sloth.SlothV1().PrometheusServiceLevels(ns).Create(ctx, SLOs, metav1.CreateOptions{}) + require.NoError(t, err) + + // Wait to be sure the controller had time for handling. + time.Sleep(500 * time.Millisecond) + + // Check. + expRule := getSLOPluginsPromOpPrometheusRule(version) expRule.Namespace = ns gotRule, err := kubeClis.Monitoring.MonitoringV1().PrometheusRules(ns).Get(ctx, expRule.Name, metav1.GetOptions{}) @@ -224,7 +247,7 @@ func TestKubernetesControllerPromOperatorGenerate(t *testing.T) { args := []string{ "--metrics-listen-addr=:0", "--hot-reload-addr=:0", - "--plugins-path=./", + "--plugins-path=./plugins", fmt.Sprintf("--namespace=%s", ns), fmt.Sprintf("--default-slo-period=%s", test.sloPeriod), } diff --git a/test/integration/k8scontroller/plugin/plugin.go b/test/integration/k8scontroller/plugins/sli/plugin1/plugin.go similarity index 100% rename from test/integration/k8scontroller/plugin/plugin.go rename to test/integration/k8scontroller/plugins/sli/plugin1/plugin.go diff --git a/test/integration/k8scontroller/plugins/slo/plugin1/plugin.go b/test/integration/k8scontroller/plugins/slo/plugin1/plugin.go new file mode 100644 index 00000000..97aaf03e --- /dev/null +++ b/test/integration/k8scontroller/plugins/slo/plugin1/plugin.go @@ -0,0 +1,46 @@ +package plugin + +import ( + "context" + "encoding/json" + + utilsdata "github.com/slok/sloth/pkg/common/utils/data" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "integration-tests/plugin1" +) + +type Config struct { + Labels map[string]string `json:"labels,omitempty"` +} + +func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + cfg := Config{} + err := json.Unmarshal(configData, &cfg) + if err != nil { + return nil, err + } + + return plugin{ + config: cfg, + }, nil +} + +type plugin struct { + config Config +} + +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + for i, r := range result.SLORules.MetadataRecRules.Rules { + if r.Record == "sloth_slo_info" { + r.Labels = utilsdata.MergeLabels(r.Labels, p.config.Labels) + result.SLORules.MetadataRecRules.Rules[i] = r + break + } + } + + return nil +} diff --git a/test/integration/prometheus/generate_test.go b/test/integration/prometheus/generate_test.go index ddfabc74..69b0c84f 100644 --- a/test/integration/prometheus/generate_test.go +++ b/test/integration/prometheus/generate_test.go @@ -85,6 +85,11 @@ func TestPrometheusGenerate(t *testing.T) { expOut: expectLoader.mustLoadExp("./testdata/out-slo-plugin.yaml.tpl"), }, + "Generate with SLO plugins should generate the correct rules for all the SLOs (Kubernetes).": { + genCmdArgs: "--input ./testdata/in-slo-plugin-k8s.yaml", + expOut: expectLoader.mustLoadExp("./testdata/out-slo-plugin-k8s.yaml.tpl"), + }, + "Generate using multifile YAML in single file should generate the correct rules for all the SLOs.": { genCmdArgs: "--input ./testdata/in-multifile.yaml", expOut: expectLoader.mustLoadExp("./testdata/out-multifile.yaml.tpl"), diff --git a/test/integration/prometheus/testdata/in-slo-plugin-k8s.yaml b/test/integration/prometheus/testdata/in-slo-plugin-k8s.yaml new file mode 100644 index 00000000..bb5fbf62 --- /dev/null +++ b/test/integration/prometheus/testdata/in-slo-plugin-k8s.yaml @@ -0,0 +1,39 @@ +apiVersion: sloth.slok.dev/v1 +kind: PrometheusServiceLevel +metadata: + name: svc + namespace: test-ns +spec: + service: "svc01" + labels: + global01k1: global01v1 + sloPlugins: + chain: + - id: "integration-tests/plugin1" + priority: 9999999 + config: {labels: {"k1": "v1", "k2": "v2"}} + - id: "integration-tests/plugin1" + priority: -999999 + config: {labels: {"k3": "v3"}} # These should be replaced because is before defaults + slos: + - name: "slo1" + objective: 99.9 + description: "This is SLO 01." + labels: + global02k1: global02v1 + sli: + events: + errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) + totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) + plugins: + chain: + - id: "integration-tests/plugin1" + config: {labels: {"k4": "v4"}} + - id: "integration-tests/plugin1" + priority: 1000 + config: {labels: {"k2": "v0", "k5": "v5"}} # k2 should be replaced by a (9999999 priority) plugin. + alerting: + pageAlert: + disable: true + ticketAlert: + disable: true diff --git a/test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl b/test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl new file mode 100644 index 00000000..5e1954fd --- /dev/null +++ b/test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl @@ -0,0 +1,187 @@ + +--- +# Code generated by Sloth ({{ .version }}): https://github.com/slok/sloth. +# DO NOT EDIT. + +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + creationTimestamp: null + labels: + app.kubernetes.io/component: SLO + app.kubernetes.io/managed-by: sloth + name: svc + namespace: test-ns +spec: + groups: + - name: sloth-slo-sli-recordings-svc01-slo1 + rules: + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[5m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[5m]))) + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 5m + record: slo:sli_error:ratio_rate5m + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[30m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[30m]))) + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 30m + record: slo:sli_error:ratio_rate30m + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1h]))) + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 1h + record: slo:sli_error:ratio_rate1h + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[2h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[2h]))) + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 2h + record: slo:sli_error:ratio_rate2h + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[6h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[6h]))) + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 6h + record: slo:sli_error:ratio_rate6h + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1d]))) + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 1d + record: slo:sli_error:ratio_rate1d + - expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[3d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[3d]))) + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 3d + record: slo:sli_error:ratio_rate3d + - expr: | + sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"}[30d]) + / ignoring (sloth_window) + count_over_time(slo:sli_error:ratio_rate5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"}[30d]) + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 30d + record: slo:sli_error:ratio_rate30d + - name: sloth-slo-meta-recordings-svc01-slo1 + rules: + - expr: vector(0.9990000000000001) + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:objective:ratio + - expr: vector(1-0.9990000000000001) + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:error_budget:ratio + - expr: vector(30) + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:time_period:days + - expr: | + slo:sli_error:ratio_rate5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:current_burn_rate:ratio + - expr: | + slo:sli_error:ratio_rate30d{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:period_burn_rate:ratio + - expr: 1 - slo:period_burn_rate:ratio{sloth_id="svc01-slo1", sloth_service="svc01", + sloth_slo="slo1"} + labels: + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:period_error_budget_remaining:ratio + - expr: vector(1) + labels: + global01k1: global01v1 + global02k1: global02v1 + k1: v1 + k2: v2 + k4: v4 + k5: v5 + sloth_id: svc01-slo1 + sloth_mode: cli-gen-k8s + sloth_objective: "99.9" + sloth_service: svc01 + sloth_slo: slo1 + sloth_spec: sloth.slok.dev/v1 + sloth_version: {{ .version }} + record: sloth_slo_info From fe7d863fbd3f485308045ef42f8e6028c0503443 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 07:52:29 +0000 Subject: [PATCH 050/173] build(deps): bump sigs.k8s.io/structured-merge-diff/v4 Bumps [sigs.k8s.io/structured-merge-diff/v4](https://github.com/kubernetes-sigs/structured-merge-diff) from 4.6.0 to 4.7.0. - [Release notes](https://github.com/kubernetes-sigs/structured-merge-diff/releases) - [Changelog](https://github.com/kubernetes-sigs/structured-merge-diff/blob/master/RELEASE.md) - [Commits](https://github.com/kubernetes-sigs/structured-merge-diff/compare/v4.6.0...v4.7.0) --- updated-dependencies: - dependency-name: sigs.k8s.io/structured-merge-diff/v4 dependency-version: 4.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0f0c380b..d8fca567 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( k8s.io/api v0.32.3 k8s.io/apimachinery v0.32.3 k8s.io/client-go v0.32.3 - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 + sigs.k8s.io/structured-merge-diff/v4 v4.7.0 ) require ( diff --git a/go.sum b/go.sum index c5c811c0..60fbd080 100644 --- a/go.sum +++ b/go.sum @@ -289,7 +289,7 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= From 1dd1231d22d15c9ba141f806e9d1a314333d95c1 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Mon, 14 Apr 2025 20:10:16 +0200 Subject: [PATCH 051/173] Fix naming on k8s plugin example Signed-off-by: Xabier Larrakoetxea --- examples/_gen/slo-plugin-k8s-getting-started.yml | 2 +- examples/slo-plugin-k8s-getting-started.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/_gen/slo-plugin-k8s-getting-started.yml b/examples/_gen/slo-plugin-k8s-getting-started.yml index de14e579..1a7987c8 100644 --- a/examples/_gen/slo-plugin-k8s-getting-started.yml +++ b/examples/_gen/slo-plugin-k8s-getting-started.yml @@ -10,7 +10,7 @@ metadata: labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth - name: sloth-slo-my-service + name: sloth-slo-my-service-with-slo-plugins namespace: monitoring spec: groups: diff --git a/examples/slo-plugin-k8s-getting-started.yml b/examples/slo-plugin-k8s-getting-started.yml index 7b79a92a..9d1c5a9c 100644 --- a/examples/slo-plugin-k8s-getting-started.yml +++ b/examples/slo-plugin-k8s-getting-started.yml @@ -1,12 +1,12 @@ -# This example shows the same example as getting-started.yml but using Sloth Kubernetes CRD. +# This example shows the same example as getting-started.yml but using Sloth Kubernetes CRD and SLO plugins. # It will generate the Prometheus rules in a Kubernetes prometheus-operator PrometheusRules CRD. # -# `sloth generate -i ./examples/k8s-getting-started.yml` +# `sloth generate --debug -i ./examples/slo-plugin-k8s-getting-started.yml` # apiVersion: sloth.slok.dev/v1 kind: PrometheusServiceLevel metadata: - name: sloth-slo-my-service + name: sloth-slo-my-service-with-slo-plugins namespace: monitoring spec: service: "myservice" From 95f393cdc260eba7f0c098dfcd1a620fdd0ccfe0 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Thu, 17 Apr 2025 12:31:43 +0200 Subject: [PATCH 052/173] Add overridePrevious as a way of resetting the declared previous chain Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 2 + ...loth.slok.dev_prometheusservicelevels.yaml | 16 ++++ internal/app/generate/generate.go | 6 +- internal/app/generate/generate_test.go | 83 +++++++++++++++++++ internal/storage/io/k8s_sloth.go | 15 +++- internal/storage/io/k8s_sloth_test.go | 14 +++- internal/storage/io/sloth.go | 14 +++- internal/storage/io/sloth_test.go | 16 +++- pkg/common/model/slo_prometheus.go | 3 +- pkg/kubernetes/api/sloth/v1/README.md | 16 +++- pkg/kubernetes/api/sloth/v1/types.go | 8 ++ .../applyconfiguration/sloth/v1/sloplugins.go | 11 ++- ...loth.slok.dev_prometheusservicelevels.yaml | 16 ++++ pkg/prometheus/api/v1/README.md | 10 ++- pkg/prometheus/api/v1/v1.go | 6 ++ 15 files changed, 221 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ff9d396..112df1c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ - SLO plugins can be loaded from FS directories recursively using `--slo-plugins-path` in the commands. - SLO plugins have a priority value to be able to order in the execution chain. - Sloth regular (non-k8s) `prometheus/v1` API support for SLO plugins at SLO group level and per SLO level. +- Sloth K8s CRD `sloth.slok.dev/v1/PrometheusServiceLevel` API support for SLO plugins at SLO group level and per SLO level. +- Allow overriding previous declared plugins (includes defaults) at SLO group and SLO level. ## [v0.12.0] - 2025-03-27 diff --git a/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml b/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml index ffdf7964..b6fc963c 100644 --- a/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml +++ b/deploy/kubernetes/helm/sloth/crds/sloth.slok.dev_prometheusservicelevels.yaml @@ -110,6 +110,14 @@ spec: - id type: object type: array + overridePrevious: + description: |- + OverridePrevious will override the previous SLO plugins declared. + Depending on where is this SLO plugins block declared will override: + - If declared at SLO group level: Overrides the default plugins. + - If declared at SLO level: Overrides the default + SLO group plugins. + The declaration order is default plugins -> SLO Group plugins -> SLO plugins. + type: boolean required: - chain type: object @@ -241,6 +249,14 @@ spec: - id type: object type: array + overridePrevious: + description: |- + OverridePrevious will override the previous SLO plugins declared. + Depending on where is this SLO plugins block declared will override: + - If declared at SLO group level: Overrides the default plugins. + - If declared at SLO level: Overrides the default + SLO group plugins. + The declaration order is default plugins -> SLO Group plugins -> SLO plugins. + type: boolean required: - chain type: object diff --git a/internal/app/generate/generate.go b/internal/app/generate/generate.go index 063a1fe3..00ae81c3 100644 --- a/internal/app/generate/generate.go +++ b/internal/app/generate/generate.go @@ -233,7 +233,11 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, sloGroup mode } // Prepare processors. - sloProcessors := append(preDefault, s.defaultPlugins...) + sloProcessors := preDefault + // Add default plugins if we don't want to override the default ones. + if !slo.Plugins.OverrideDefaultPlugins { + sloProcessors = append(sloProcessors, s.defaultPlugins...) + } sloProcessors = append(sloProcessors, postDefault...) req := &SLOProcessorRequest{ diff --git a/internal/app/generate/generate_test.go b/internal/app/generate/generate_test.go index 5dcb0178..1f5964e1 100644 --- a/internal/app/generate/generate_test.go +++ b/internal/app/generate/generate_test.go @@ -804,6 +804,89 @@ or }, }, }, + + "Having SLO plugins with default plugin override should execute only the configured plugins and ignore default plugin execution.": { + mocks: func(mspg *generatemock.SLOPluginGetter) { + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin1").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin1", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertRuleAppender{rule: rulefmt.Rule{Expr: "test1"}}, nil + }, + }, nil) + }, + req: generate.Request{ + Info: model.Info{ + Version: "test-ver", + Mode: model.ModeTest, + Spec: "test-spec", + }, + SLOGroup: model.PromSLOGroup{SLOs: []model.PromSLO{ + { + ID: "test-id", + Name: "test-name", + Service: "test-svc", + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `rate(my_metric{error="true"}[{{.window}}])`, + TotalQuery: `rate(my_metric[{{.window}}])`, + }, + }, + TimeWindow: 30 * 24 * time.Hour, + Objective: 99.9, + Labels: map[string]string{"test_label": "label_1"}, + PageAlertMeta: model.PromAlertMeta{ + Name: "p_alert_test_name", + Labels: map[string]string{"p_alert_label": "p_label_al_1"}, + Annotations: map[string]string{"p_alert_annot": "p_label_an_1"}, + }, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, + Plugins: model.SLOPlugins{ + OverrideDefaultPlugins: true, + Plugins: []model.PromSLOPluginMetadata{ + {ID: "test-plugin1", Priority: 10}, + }, + }, + }, + }}, + }, + expResp: generate.Response{ + PrometheusSLOs: []generate.SLOResult{ + { + SLO: model.PromSLO{ + ID: "test-id", + Name: "test-name", + Service: "test-svc", + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `rate(my_metric{error="true"}[{{.window}}])`, + TotalQuery: `rate(my_metric[{{.window}}])`, + }, + }, + TimeWindow: 30 * 24 * time.Hour, + Objective: 99.9, + Labels: map[string]string{"test_label": "label_1"}, + PageAlertMeta: model.PromAlertMeta{ + Name: "p_alert_test_name", + Labels: map[string]string{"p_alert_label": "p_label_al_1"}, + Annotations: map[string]string{"p_alert_annot": "p_label_an_1"}, + }, + TicketAlertMeta: model.PromAlertMeta{Disable: true}, + Plugins: model.SLOPlugins{ + OverrideDefaultPlugins: true, + Plugins: []model.PromSLOPluginMetadata{ + {ID: "test-plugin1", Priority: 10}, + }, + }, + }, + SLORules: model.PromSLORules{ + AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + {Expr: "test1"}, + }}, + }, + }, + }, + }, + }, } for name, test := range tests { diff --git a/internal/storage/io/k8s_sloth.go b/internal/storage/io/k8s_sloth.go index 29fc604e..3d64b3a3 100644 --- a/internal/storage/io/k8s_sloth.go +++ b/internal/storage/io/k8s_sloth.go @@ -89,8 +89,10 @@ func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, plug slos := make([]model.PromSLO, 0, len(kspec.Spec.SLOs)) spec := kspec.Spec + groupOverridePlugins := false var groupSLOPlugins []model.PromSLOPluginMetadata if spec.SLOPlugins != nil { + groupOverridePlugins = spec.SLOPlugins.OverridePrevious for _, plugin := range spec.SLOPlugins.Chain { groupSLOPlugins = append(groupSLOPlugins, model.PromSLOPluginMetadata{ ID: plugin.ID, @@ -102,7 +104,15 @@ func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, plug for _, specSLO := range kspec.Spec.SLOs { plugins := append([]model.PromSLOPluginMetadata{}, groupSLOPlugins...) // Add group plugins if any. + + overridePlugins := groupOverridePlugins if specSLO.Plugins != nil { + // If we need to override the previous plugins at SLO level we need to remove the group plugins. + if specSLO.Plugins.OverridePrevious { + plugins = []model.PromSLOPluginMetadata{} + overridePlugins = true + } + for _, plugin := range specSLO.Plugins.Chain { plugins = append(plugins, model.PromSLOPluginMetadata{ ID: plugin.ID, @@ -122,7 +132,10 @@ func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, plug Labels: utilsdata.MergeLabels(spec.Labels, specSLO.Labels), PageAlertMeta: model.PromAlertMeta{Disable: true}, TicketAlertMeta: model.PromAlertMeta{Disable: true}, - Plugins: model.SLOPlugins{Plugins: plugins}, + Plugins: model.SLOPlugins{ + OverrideDefaultPlugins: overridePlugins, + Plugins: plugins, + }, } // Set SLIs. diff --git a/internal/storage/io/k8s_sloth_test.go b/internal/storage/io/k8s_sloth_test.go index 2c750a7a..dfb9e25a 100644 --- a/internal/storage/io/k8s_sloth_test.go +++ b/internal/storage/io/k8s_sloth_test.go @@ -257,6 +257,10 @@ spec: events: errorQuery: test_expr_error_1 totalQuery: test_expr_total_1 + plugins: + overridePrevious: true + chain: + - {id: test_plugin1, priority: 100, config: {k1: v1, k3: true}} alerting: name: testAlert labels: @@ -340,9 +344,9 @@ spec: }, }, Plugins: model.SLOPlugins{ + OverrideDefaultPlugins: true, Plugins: []model.PromSLOPluginMetadata{ - {ID: "test_plugin0", Priority: -100, Config: json.RawMessage([]byte(`{"k1":42}`))}, - {ID: "test_plugin2", Config: json.RawMessage([]byte(`{"k1":{"k2":"v2"}}`))}, + {ID: "test_plugin1", Priority: 100, Config: json.RawMessage([]byte(`{"k1":"v1","k3":true}`))}, }, }, }, @@ -395,6 +399,12 @@ spec: ErrorQuery: "test_expr_error_1", TotalQuery: "test_expr_total_1", }}, + Plugins: &kubeslothv1.SLOPlugins{ + OverridePrevious: true, + Chain: []kubeslothv1.SLOPlugin{ + {ID: "test_plugin1", Priority: 100, Config: []byte(`{"k1":"v1","k3":true}`)}, + }, + }, Alerting: kubeslothv1.Alerting{ Name: "testAlert", Labels: map[string]string{"tier": "1"}, diff --git a/internal/storage/io/sloth.go b/internal/storage/io/sloth.go index 41d1affc..96e94d8f 100644 --- a/internal/storage/io/sloth.go +++ b/internal/storage/io/sloth.go @@ -73,6 +73,7 @@ func (l SlothPrometheusYAMLSpecLoader) mapSpecToModel(ctx context.Context, spec // Get group plugins if any. var groupSLOPlugins []model.PromSLOPluginMetadata + groupOverridePlugins := spec.SLOPlugins.OverridePrevious for _, plugin := range spec.SLOPlugins.Chain { groupSLOPlugins = append(groupSLOPlugins, model.PromSLOPluginMetadata{ ID: plugin.ID, @@ -83,6 +84,14 @@ func (l SlothPrometheusYAMLSpecLoader) mapSpecToModel(ctx context.Context, spec for _, specSLO := range spec.SLOs { plugins := append([]model.PromSLOPluginMetadata{}, groupSLOPlugins...) // Add group plugins if any. + + // If we need to override the previous plugins at SLO level we need to remove the group plugins. + overridePlugins := groupOverridePlugins + if specSLO.Plugins.OverridePrevious { + plugins = []model.PromSLOPluginMetadata{} + overridePlugins = true + } + for _, plugin := range specSLO.Plugins.Chain { plugins = append(plugins, model.PromSLOPluginMetadata{ ID: plugin.ID, @@ -101,7 +110,10 @@ func (l SlothPrometheusYAMLSpecLoader) mapSpecToModel(ctx context.Context, spec Labels: utilsdata.MergeLabels(spec.Labels, specSLO.Labels), PageAlertMeta: model.PromAlertMeta{Disable: true}, TicketAlertMeta: model.PromAlertMeta{Disable: true}, - Plugins: model.SLOPlugins{Plugins: plugins}, + Plugins: model.SLOPlugins{ + OverrideDefaultPlugins: overridePlugins, + Plugins: plugins, + }, } // Set SLIs. diff --git a/internal/storage/io/sloth_test.go b/internal/storage/io/sloth_test.go index 1232ca1a..41a1809d 100644 --- a/internal/storage/io/sloth_test.go +++ b/internal/storage/io/sloth_test.go @@ -273,6 +273,11 @@ slos: events: error_query: test_expr_error_1 total_query: test_expr_total_1 + plugins: + overridePrevious: true + chain: + - {id: test_plugin1, priority: 100, config: {k1: v1, k3: true}} + alerting: name: testAlert labels: @@ -291,6 +296,7 @@ slos: channel: "#a-not-so-important" annotations: message: "This is not very important." + - name: "slo2" labels: category: test2 @@ -356,9 +362,9 @@ slos: }, }, Plugins: model.SLOPlugins{ + OverrideDefaultPlugins: true, Plugins: []model.PromSLOPluginMetadata{ - {ID: "test_plugin0", Priority: -100, Config: json.RawMessage([]byte(`{"k1":42}`))}, - {ID: "test_plugin2", Config: json.RawMessage([]byte(`{"k1":{"k2":"v2"}}`))}, + {ID: "test_plugin1", Priority: 100, Config: json.RawMessage([]byte(`{"k1":"v1","k3":true}`))}, }, }, }, @@ -412,6 +418,12 @@ slos: TotalQuery: `test_expr_total_1`, }, }, + Plugins: v1.SLOPlugins{ + OverridePrevious: true, + Chain: []v1.SLOPlugin{ + {ID: "test_plugin1", Priority: 100, Config: []byte(`{"k1":"v1","k3":true}`)}, + }, + }, Alerting: v1.Alerting{Name: "testAlert", Labels: map[string]string{"tier": "1"}, Annotations: map[string]string{"runbook": "http://whatever.com"}, diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index 1884e3c2..09b8031e 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -57,7 +57,8 @@ type PromSLO struct { } type SLOPlugins struct { - Plugins []PromSLOPluginMetadata + OverrideDefaultPlugins bool + Plugins []PromSLOPluginMetadata } type PromSLOPluginMetadata struct { diff --git a/pkg/kubernetes/api/sloth/v1/README.md b/pkg/kubernetes/api/sloth/v1/README.md index eaa650eb..83353c07 100755 --- a/pkg/kubernetes/api/sloth/v1/README.md +++ b/pkg/kubernetes/api/sloth/v1/README.md @@ -232,7 +232,7 @@ func (in *PrometheusServiceLevel) DeepCopyObject() runtime.Object DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -## type [PrometheusServiceLevelList]() +## type [PrometheusServiceLevelList]() \+k8s:deepcopy\-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -320,7 +320,7 @@ func (in *PrometheusServiceLevelSpec) DeepCopyInto(out *PrometheusServiceLevelSp DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type [PrometheusServiceLevelStatus]() +## type [PrometheusServiceLevelStatus]() @@ -567,7 +567,7 @@ func (in *SLO) DeepCopyInto(out *SLO) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type [SLOPlugin]() +## type [SLOPlugin]() SLOPlugin is a plugin that will be used on the chain of plugins for the SLO generation. @@ -614,12 +614,20 @@ func (in *SLOPlugin) DeepCopyInto(out *SLOPlugin) DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non\-nil. -## type [SLOPlugins]() +## type [SLOPlugins]() SLOPlugins are the list plugins that will be used on the process of SLOs for the rules generation. ```go type SLOPlugins struct { + // OverridePrevious will override the previous SLO plugins declared. + // Depending on where is this SLO plugins block declared will override: + // - If declared at SLO group level: Overrides the default plugins. + // - If declared at SLO level: Overrides the default + SLO group plugins. + // The declaration order is default plugins -> SLO Group plugins -> SLO plugins. + // +optional + OverridePrevious bool `json:"overridePrevious,omitempty"` + // chain ths the list of plugin chain to add to the SLO generation. Chain []SLOPlugin `json:"chain"` } diff --git a/pkg/kubernetes/api/sloth/v1/types.go b/pkg/kubernetes/api/sloth/v1/types.go index bdc25165..07007a2b 100644 --- a/pkg/kubernetes/api/sloth/v1/types.go +++ b/pkg/kubernetes/api/sloth/v1/types.go @@ -182,6 +182,14 @@ type Alert struct { // SLOPlugins are the list plugins that will be used on the process of SLOs for the // rules generation. type SLOPlugins struct { + // OverridePrevious will override the previous SLO plugins declared. + // Depending on where is this SLO plugins block declared will override: + // - If declared at SLO group level: Overrides the default plugins. + // - If declared at SLO level: Overrides the default + SLO group plugins. + // The declaration order is default plugins -> SLO Group plugins -> SLO plugins. + // +optional + OverridePrevious bool `json:"overridePrevious,omitempty"` + // chain ths the list of plugin chain to add to the SLO generation. Chain []SLOPlugin `json:"chain"` } diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugins.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugins.go index 5bc0e656..e17c3624 100644 --- a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugins.go +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/sloplugins.go @@ -5,7 +5,8 @@ package v1 // SLOPluginsApplyConfiguration represents a declarative configuration of the SLOPlugins type for use // with apply. type SLOPluginsApplyConfiguration struct { - Chain []SLOPluginApplyConfiguration `json:"chain,omitempty"` + OverridePrevious *bool `json:"overridePrevious,omitempty"` + Chain []SLOPluginApplyConfiguration `json:"chain,omitempty"` } // SLOPluginsApplyConfiguration constructs a declarative configuration of the SLOPlugins type for use with @@ -14,6 +15,14 @@ func SLOPlugins() *SLOPluginsApplyConfiguration { return &SLOPluginsApplyConfiguration{} } +// WithOverridePrevious sets the OverridePrevious field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the OverridePrevious field is set to the value of the last call. +func (b *SLOPluginsApplyConfiguration) WithOverridePrevious(value bool) *SLOPluginsApplyConfiguration { + b.OverridePrevious = &value + return b +} + // WithChain adds the given value to the Chain field in the declarative configuration // and returns the receiver, so that objects can be build by chaining "With" function invocations. // If called multiple times, values provided by each call will be appended to the Chain field. diff --git a/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml b/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml index ffdf7964..b6fc963c 100644 --- a/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml +++ b/pkg/kubernetes/gen/crd/sloth.slok.dev_prometheusservicelevels.yaml @@ -110,6 +110,14 @@ spec: - id type: object type: array + overridePrevious: + description: |- + OverridePrevious will override the previous SLO plugins declared. + Depending on where is this SLO plugins block declared will override: + - If declared at SLO group level: Overrides the default plugins. + - If declared at SLO level: Overrides the default + SLO group plugins. + The declaration order is default plugins -> SLO Group plugins -> SLO plugins. + type: boolean required: - chain type: object @@ -241,6 +249,14 @@ spec: - id type: object type: array + overridePrevious: + description: |- + OverridePrevious will override the previous SLO plugins declared. + Depending on where is this SLO plugins block declared will override: + - If declared at SLO group level: Overrides the default plugins. + - If declared at SLO level: Overrides the default + SLO group plugins. + The declaration order is default plugins -> SLO Group plugins -> SLO plugins. + type: boolean required: - chain type: object diff --git a/pkg/prometheus/api/v1/README.md b/pkg/prometheus/api/v1/README.md index a17a0741..9c9d239a 100755 --- a/pkg/prometheus/api/v1/README.md +++ b/pkg/prometheus/api/v1/README.md @@ -216,7 +216,7 @@ type SLO struct { ``` -## type [SLOPlugin]() +## type [SLOPlugin]() SLOPlugin is a plugin that will be used on the chain of plugins for the SLO generation. @@ -237,12 +237,18 @@ type SLOPlugin struct { ``` -## type [SLOPlugins]() +## type [SLOPlugins]() SLOPlugins are the list plugins that will be used on the process of SLOs for the rules generation. ```go type SLOPlugins struct { + // OverridePrevious will override the previous SLO plugins declared. + // Depending on where is this SLO plugins block declared will override: + // - If declared at SLO group level: Overrides the default plugins. + // - If declared at SLO level: Overrides the default + SLO group plugins. + // The declaration order is default plugins -> SLO Group plugins -> SLO plugins. + OverridePrevious bool `json:"overridePrevious,omitempty"` // chain ths the list of plugin chain to add to the SLO generation. Chain []SLOPlugin `json:"chain"` } diff --git a/pkg/prometheus/api/v1/v1.go b/pkg/prometheus/api/v1/v1.go index 30286660..e18b4b9f 100644 --- a/pkg/prometheus/api/v1/v1.go +++ b/pkg/prometheus/api/v1/v1.go @@ -170,6 +170,12 @@ type Alert struct { // SLOPlugins are the list plugins that will be used on the process of SLOs for the // rules generation. type SLOPlugins struct { + // OverridePrevious will override the previous SLO plugins declared. + // Depending on where is this SLO plugins block declared will override: + // - If declared at SLO group level: Overrides the default plugins. + // - If declared at SLO level: Overrides the default + SLO group plugins. + // The declaration order is default plugins -> SLO Group plugins -> SLO plugins. + OverridePrevious bool `json:"overridePrevious,omitempty"` // chain ths the list of plugin chain to add to the SLO generation. Chain []SLOPlugin `json:"chain"` } From 074cd5733f4bd337da4925178be9bca254226c04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 07:13:01 +0000 Subject: [PATCH 053/173] build(deps): bump github.com/prometheus-operator/prometheus-operator/pkg/client Bumps [github.com/prometheus-operator/prometheus-operator/pkg/client](https://github.com/prometheus-operator/prometheus-operator) from 0.81.0 to 0.82.0. - [Release notes](https://github.com/prometheus-operator/prometheus-operator/releases) - [Changelog](https://github.com/prometheus-operator/prometheus-operator/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus-operator/prometheus-operator/compare/v0.81.0...v0.82.0) --- updated-dependencies: - dependency-name: github.com/prometheus-operator/prometheus-operator/pkg/client dependency-version: 0.82.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 24 ++++++++++++------------ go.sum | 48 ++++++++++++++++++++++++------------------------ 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index d8fca567..9585dcc5 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/alecthomas/kingpin/v2 v2.4.0 github.com/go-playground/validator/v10 v10.26.0 github.com/oklog/run v1.1.0 - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.81.0 - github.com/prometheus-operator/prometheus-operator/pkg/client v0.81.0 + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.0 + github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/common v0.63.0 github.com/prometheus/prometheus v0.302.2-0.20250320225832-3d603d19575e @@ -33,7 +33,7 @@ require ( github.com/edsrzf/mmap-go v1.2.0 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -70,17 +70,17 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.36.0 // indirect + golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect golang.org/x/time v0.11.0 // indirect golang.org/x/tools v0.31.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -88,7 +88,7 @@ require ( k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect - sigs.k8s.io/controller-runtime v0.20.3 // indirect + sigs.k8s.io/controller-runtime v0.20.4 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index 60fbd080..f1f6c88e 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1S github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -140,10 +140,10 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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-operator/prometheus-operator/pkg/apis/monitoring v0.81.0 h1:mSii7z+TihzdeULnGjLnNikgtDbeViY/wW8s3430rhE= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.81.0/go.mod h1:YfnEQzw7tUQa0Sjiz8V6QFc6JUGE+i5wybsjc3EOKn8= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.81.0 h1:z8ETgiD2hThJi3+3S8eKAbC9/pwPq1kGt8HkeGlDstw= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.81.0/go.mod h1:EKK9z5OIxxwaCZmgaidiIfvy6mF7x8M3AJHmxwx+QB4= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.0 h1:Ee6zu4IR/WKYEcYHL4+gbC1A3GAzlHWxSjjMyRVBHYw= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.0/go.mod h1:hY5yoQsoIalncoxYqXXCDL5y7f+GGYYlW9Bi2IdU5KY= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0 h1:g/wIdMrRyQ1Fac8plyQMXzky8R9kOi243tQi3a2YkyU= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0/go.mod h1:yEp9v3FEYT+iR2ujaXFVS8ugTIQk0Mx+wwXGXoF5az8= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= @@ -204,8 +204,8 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -214,27 +214,27 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn 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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -254,8 +254,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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= @@ -282,8 +282,8 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.20.3 h1:I6Ln8JfQjHH7JbtCD2HCYHoIzajoRxPNuvhvcDbZgkI= -sigs.k8s.io/controller-runtime v0.20.3/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= From 65916a5959f6ad0de727331d6395c88eba56d5b6 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 19 Apr 2025 08:41:44 +0200 Subject: [PATCH 054/173] Simplify and improve validator Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 2 + go.mod | 6 - go.sum | 12 - .../plugin/slo/core/validate_v1/plugin.go | 4 +- internal/pluginengine/slo/custom/custom.go | 2 +- ...b_com-slok-sloth-pkg-common-conventions.go | 2 + .../github_com-slok-sloth-pkg-common-model.go | 1 - ...ub_com-slok-sloth-pkg-common-validation.go | 48 ++++ pkg/common/conventions/conventions.go | 14 ++ pkg/common/errors/errors.go | 3 + pkg/common/model/slo_prometheus.go | 222 +----------------- pkg/common/validation/promql.go | 80 +++++++ pkg/common/validation/slo.go | 191 +++++++++++++++ .../slo_test.go} | 167 +++++++++---- 14 files changed, 481 insertions(+), 273 deletions(-) create mode 100644 internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-validation.go create mode 100644 pkg/common/conventions/conventions.go create mode 100644 pkg/common/validation/promql.go create mode 100644 pkg/common/validation/slo.go rename pkg/common/{model/slo_prometheus_test.go => validation/slo_test.go} (58%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 112df1c3..b026a6eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,13 @@ - (BREAKING) Internally Sloth (not k8s) prometheusServiceLevel uses k8s `k8s.io/apimachinery/pkg/util/yaml` lib for unmarshaling YAML instead of `gopkg.in/yaml.v2`. - Core SLO validation and SLO rules generation migrated to SLO plugins. - (BREAKING) `--sli-plugins-path`, `--slo-plugins-path`, `-m` args and it's env vars `SLOTH_SLI_PLUGINS_PATH`and `SLOTH_SLO_PLUGINS_PATH` have been removed in favor or `--plugins-path`, `-p` and it's env var `SLOTH_PLUGINS_PATH` that discovers and loads SLI and SLO plugins with a single flag. +- Simplify validation and improve validation message by using custom logic instead of `go-playground/validator`. ### Added - Sloth domain models can be imported in Go apps using `github.com/slok/sloth/pkg/common/model`. - Sloth conventions can be imported in Go apps using `github.com/slok/sloth/pkg/common/conventions`. +- Sloth SLO validation logic can be imported in Go apps using `github.com/slok/sloth/pkg/common/validation`. - A new SLO rule generation plugin system has been added to be able to change/extend the SLO rule generation process. - SLO plugins can be loaded from FS directories recursively using `--slo-plugins-path` in the commands. - SLO plugins have a priority value to be able to order in the execution chain. diff --git a/go.mod b/go.mod index 9585dcc5..c28cf82b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.24.0 require ( github.com/OpenSLO/oslo v0.12.0 github.com/alecthomas/kingpin/v2 v2.4.0 - github.com/go-playground/validator/v10 v10.26.0 github.com/oklog/run v1.1.0 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.0 github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0 @@ -34,14 +33,11 @@ require ( github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect github.com/fxamacker/cbor/v2 v2.8.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect @@ -52,7 +48,6 @@ require ( github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -70,7 +65,6 @@ require ( go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/oauth2 v0.29.0 // indirect diff --git a/go.sum b/go.sum index f1f6c88e..c63558f6 100644 --- a/go.sum +++ b/go.sum @@ -42,8 +42,6 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -55,14 +53,6 @@ github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -111,8 +101,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/internal/plugin/slo/core/validate_v1/plugin.go b/internal/plugin/slo/core/validate_v1/plugin.go index 226a8cab..aed3c8df 100644 --- a/internal/plugin/slo/core/validate_v1/plugin.go +++ b/internal/plugin/slo/core/validate_v1/plugin.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/slok/sloth/pkg/common/validation" pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" ) @@ -24,8 +25,7 @@ type plugin struct { } func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { - // TODO(slok): Should we stop using validator libraries and just use our own simple validation logic created here? - err := request.SLO.Validate() + err := validation.ValidateSLO(request.SLO, validation.PromQLDialectValidator) if err != nil { return fmt.Errorf("invalid slo %q: %w", request.SLO.ID, err) } diff --git a/internal/pluginengine/slo/custom/custom.go b/internal/pluginengine/slo/custom/custom.go index f4728fa7..b00b14f0 100644 --- a/internal/pluginengine/slo/custom/custom.go +++ b/internal/pluginengine/slo/custom/custom.go @@ -5,7 +5,7 @@ import ( ) //go:generate yaegi extract --name custom github.com/prometheus/common/model github.com/prometheus/prometheus/model/rulefmt github.com/prometheus/prometheus/promql/parser -//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/prometheus/plugin/slo/v1 github.com/slok/sloth/pkg/common/conventions github.com/slok/sloth/pkg/common/model github.com/slok/sloth/pkg/common/utils/data github.com/slok/sloth/pkg/common/utils/prometheus +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/prometheus/plugin/slo/v1 github.com/slok/sloth/pkg/common/conventions github.com/slok/sloth/pkg/common/model github.com/slok/sloth/pkg/common/utils/data github.com/slok/sloth/pkg/common/utils/prometheus github.com/slok/sloth/pkg/common/validation // Symbols variable stores the map of custom Yaegi symbols per package. var Symbols = map[string]map[string]reflect.Value{} diff --git a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go index f31d3f03..bbc66b2b 100644 --- a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go +++ b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go @@ -14,6 +14,7 @@ func init() { // function, constant and variable definitions "GetSLIErrorMetric": reflect.ValueOf(conventions.GetSLIErrorMetric), "GetSLOIDPromLabels": reflect.ValueOf(conventions.GetSLOIDPromLabels), + "NameRegexp": reflect.ValueOf(&conventions.NameRegexp).Elem(), "PromSLIErrorMetricFmt": reflect.ValueOf(constant.MakeFromLiteral("\"slo:sli_error:ratio_rate%s\"", token.STRING, 0)), "PromSLOIDLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_id\"", token.STRING, 0)), "PromSLOModeLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_mode\"", token.STRING, 0)), @@ -24,5 +25,6 @@ func init() { "PromSLOSpecLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_spec\"", token.STRING, 0)), "PromSLOVersionLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_version\"", token.STRING, 0)), "PromSLOWindowLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_window\"", token.STRING, 0)), + "TplWindowRegex": reflect.ValueOf(&conventions.TplWindowRegex).Elem(), } } diff --git a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go index 5c73c571..fe867ecc 100644 --- a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go +++ b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go @@ -18,7 +18,6 @@ func init() { "ModeControllerGenKubernetes": reflect.ValueOf(constant.MakeFromLiteral("\"ctrl-gen-k8s\"", token.STRING, 0)), "ModeTest": reflect.ValueOf(constant.MakeFromLiteral("\"test\"", token.STRING, 0)), "PageAlertSeverity": reflect.ValueOf(model.PageAlertSeverity), - "PromQueryTPLKeyWindow": reflect.ValueOf(constant.MakeFromLiteral("\"window\"", token.STRING, 0)), "TicketAlertSeverity": reflect.ValueOf(model.TicketAlertSeverity), "UnknownAlertSeverity": reflect.ValueOf(model.UnknownAlertSeverity), diff --git a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-validation.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-validation.go new file mode 100644 index 00000000..2e61afd5 --- /dev/null +++ b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-validation.go @@ -0,0 +1,48 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/validation'. DO NOT EDIT. + +package custom + +import ( + "github.com/slok/sloth/pkg/common/validation" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/common/validation/validation"] = map[string]reflect.Value{ + // function, constant and variable definitions + "PromQLDialectValidator": reflect.ValueOf(validation.PromQLDialectValidator), + "ValidateSLO": reflect.ValueOf(validation.ValidateSLO), + + // type definitions + "SLODialectValidator": reflect.ValueOf((*validation.SLODialectValidator)(nil)), + + // interface wrapper definitions + "_SLODialectValidator": reflect.ValueOf((*_github_com_slok_sloth_pkg_common_validation_SLODialectValidator)(nil)), + } +} + +// _github_com_slok_sloth_pkg_common_validation_SLODialectValidator is an interface wrapper for SLODialectValidator type +type _github_com_slok_sloth_pkg_common_validation_SLODialectValidator struct { + IValue interface{} + WValidateAnnotationKey func(k string) error + WValidateAnnotationValue func(k string) error + WValidateLabelKey func(k string) error + WValidateLabelValue func(k string) error + WValidateQueryExpression func(queryExpression string) error +} + +func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateAnnotationKey(k string) error { + return W.WValidateAnnotationKey(k) +} +func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateAnnotationValue(k string) error { + return W.WValidateAnnotationValue(k) +} +func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateLabelKey(k string) error { + return W.WValidateLabelKey(k) +} +func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateLabelValue(k string) error { + return W.WValidateLabelValue(k) +} +func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateQueryExpression(queryExpression string) error { + return W.WValidateQueryExpression(queryExpression) +} diff --git a/pkg/common/conventions/conventions.go b/pkg/common/conventions/conventions.go new file mode 100644 index 00000000..ff07e61a --- /dev/null +++ b/pkg/common/conventions/conventions.go @@ -0,0 +1,14 @@ +package conventions + +import "regexp" + +var ( + // NameRegexp is the regex to validate SLO, SLI and in general safe names and IDs. + // Names must: + // - Start and end with an alphanumeric. + // - Contain alphanumeric, `.`, '_', and '-'. + NameRegexp = regexp.MustCompile(`^[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]$`) + + // TplWindowRegex is the regex to match the {{ .window }} template variable. + TplWindowRegex = regexp.MustCompile(`{{ *\.window *}}`) +) diff --git a/pkg/common/errors/errors.go b/pkg/common/errors/errors.go index 6f66e27d..954bade8 100644 --- a/pkg/common/errors/errors.go +++ b/pkg/common/errors/errors.go @@ -9,4 +9,7 @@ var ( // ErrNotFound will be used when a resource has not been found. ErrNotFound = fmt.Errorf("resource not found") + + // ErrRequired will be used when a required field is not set. + ErrRequired = fmt.Errorf("required") ) diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index 09b8031e..7a63a5c9 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -1,18 +1,10 @@ package model import ( - "bytes" - "fmt" - "reflect" - "regexp" - "text/template" "time" openslov1alpha "github.com/OpenSLO/oslo/pkg/manifest/v1alpha" - "github.com/go-playground/validator/v10" - prommodel "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/rulefmt" - promqlparser "github.com/prometheus/prometheus/promql/parser" k8sprometheusv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" prometheusv1 "github.com/slok/sloth/pkg/prometheus/api/v1" @@ -25,32 +17,32 @@ type PromSLI struct { } type PromSLIRaw struct { - ErrorRatioQuery string `validate:"required,prom_expr,template_vars"` + ErrorRatioQuery string } type PromSLIEvents struct { - ErrorQuery string `validate:"required,prom_expr,template_vars"` - TotalQuery string `validate:"required,prom_expr,template_vars"` + ErrorQuery string + TotalQuery string } // AlertMeta is the metadata of an alert settings. type PromAlertMeta struct { Disable bool - Name string `validate:"required_if_enabled"` - Labels map[string]string `validate:"dive,keys,prom_label_key,endkeys,required,prom_label_value"` - Annotations map[string]string `validate:"dive,keys,prom_annot_key,endkeys,required"` + Name string + Labels map[string]string + Annotations map[string]string } // PromSLO represents a service level objective configuration. type PromSLO struct { - ID string `validate:"required,name"` - Name string `validate:"required,name"` + ID string + Name string Description string - Service string `validate:"required,name"` - SLI PromSLI `validate:"required"` - TimeWindow time.Duration `validate:"required"` - Objective float64 `validate:"gt=0,lte=100"` - Labels map[string]string `validate:"dive,keys,prom_label_key,endkeys,required,prom_label_value"` + Service string + SLI PromSLI + TimeWindow time.Duration + Objective float64 + Labels map[string]string PageAlertMeta PromAlertMeta TicketAlertMeta PromAlertMeta Plugins SLOPlugins @@ -80,194 +72,6 @@ type PromSLOGroupSource struct { OpenSLOV1Alpha *openslov1alpha.SLO } -// Validate validates the SLO. -func (s PromSLO) Validate() error { - return modelSpecValidate.Struct(s) -} - -var modelSpecValidate = func() *validator.Validate { - v := validator.New() - - // More information on prometheus validators logic: https://github.com/prometheus/prometheus/blob/df80dc4d3970121f2f76cba79050983ffb3cdbb0/pkg/rulefmt/rulefmt.go#L188-L208 - mustRegisterValidation(v, "prom_expr", validatePromExpression) - mustRegisterValidation(v, "prom_label_key", validatePromLabelKey) - mustRegisterValidation(v, "prom_label_value", validatePromLabelValue) - mustRegisterValidation(v, "prom_annot_key", validatePromAnnotKey) - mustRegisterValidation(v, "name", validateName) - mustRegisterValidation(v, "required_if_enabled", validateRequiredEnabledAlertName) - mustRegisterValidation(v, "template_vars", validateTemplateVars) - v.RegisterStructValidation(validateOneSLI, PromSLI{}) - v.RegisterStructValidation(validateSLIEvents, PromSLIEvents{}) - return v -}() - -// mustRegisterValidation is a helper so we panic on start if we can't register a validator. -func mustRegisterValidation(v *validator.Validate, tag string, fn validator.Func) { - err := v.RegisterValidation(tag, fn) - if err != nil { - panic(err) - } -} - -// validatePromAnnotKey implements validator.CustomTypeFunc by validating -// a prometheus annotation key. -func validatePromAnnotKey(fl validator.FieldLevel) bool { - k, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - return prommodel.LabelName(k).IsValid() -} - -// validatePromLabel implements validator.CustomTypeFunc by validating -// a prometheus label key. -func validatePromLabelKey(fl validator.FieldLevel) bool { - k, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - return prommodel.LabelName(k).IsValid() && k != prommodel.MetricNameLabel -} - -// validatePromLabelValue implements validator.CustomTypeFunc by validating -// a prometheus label value. -func validatePromLabelValue(fl validator.FieldLevel) bool { - v, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - return prommodel.LabelValue(v).IsValid() -} - -var promExprTplAllowedFakeData = map[string]string{ - "window": "1m", -} - -// validatePromExpression implements validator.CustomTypeFunc by validating -// a prometheus expression. -func validatePromExpression(fl validator.FieldLevel) bool { - expr, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - // The expressions set by users can have some allowed templated data - // we are rendering the expression with fake data so prometheus can - // have a final expr and check if is correct. - tpl, err := template.New("expr").Parse(expr) - if err != nil { - return false - } - - var tplB bytes.Buffer - err = tpl.Execute(&tplB, promExprTplAllowedFakeData) - if err != nil { - return false - } - - _, err = promqlparser.ParseExpr(tplB.String()) - return err == nil -} - -// Names must: -// - Start and end with an alphanumeric. -// - Contain alphanumeric, `.`, '_', and '-'. -var ( - nameRegexp = regexp.MustCompile("^[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]$") -) - -// validateName implements validator.CustomTypeFunc by validating -// a regular name. -func validateName(fl validator.FieldLevel) bool { - s, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - return nameRegexp.MatchString(s) -} - -func validateRequiredEnabledAlertName(fl validator.FieldLevel) bool { - alertMeta, ok := fl.Parent().Interface().(PromAlertMeta) - if !ok { - return false - } - - if alertMeta.Disable { - return true - } - - return alertMeta.Name != "" -} - -const PromQueryTPLKeyWindow = "window" - -var tplWindowRegex = regexp.MustCompile(fmt.Sprintf(`{{ *\.%s *}}`, PromQueryTPLKeyWindow)) - -// validateTemplateVars implements validator.CustomTypeFunc by validating -// an SLI template has all the required fields. -func validateTemplateVars(fl validator.FieldLevel) bool { - v, ok := fl.Field().Interface().(string) - if !ok { - return false - } - - return tplWindowRegex.MatchString(v) -} - -// validateSLIEvents validates that both SLI event queries are different. -func validateSLIEvents(sl validator.StructLevel) { - s, ok := sl.Current().Interface().(PromSLIEvents) - if !ok { - sl.ReportError(s, "", "SLIEvents", "not_sli_events", "") - return - } - - // If empty we don't need to check. - if s.ErrorQuery == "" || s.TotalQuery == "" { - return - } - - // If different, they are valid. - if s.ErrorQuery == s.TotalQuery { - sl.ReportError(s, "", "", "sli_events_queries_different", "") - return - } -} - -// validateOneSLI validates only one SLI type is set and configured. -func validateOneSLI(sl validator.StructLevel) { - sli, ok := sl.Current().Interface().(PromSLI) - if !ok { - sl.ReportError(sli, "", "SLI", "not_sli", "") - return - } - - // Check only one SLI type is set. - sliSet := false - sliType := reflect.ValueOf(sli) - strNumFields := sliType.NumField() - for i := 0; i < strNumFields; i++ { - f := sliType.Field(i) - if f.IsNil() { - continue - } - // We already have one SLI type set. - if sliSet { - sl.ReportError(sli, "", "", "one_sli_type", "") - } - sliSet = true - } - - // No SLI types set. - if !sliSet { - sl.ReportError(sli, "", "", "sli_type_required", "") - } -} - // PromSLORules are the prometheus rules required by an SLO. type PromSLORules struct { SLIErrorRecRules PromRuleGroup diff --git a/pkg/common/validation/promql.go b/pkg/common/validation/promql.go new file mode 100644 index 00000000..6dc0e32f --- /dev/null +++ b/pkg/common/validation/promql.go @@ -0,0 +1,80 @@ +package validation + +import ( + "bytes" + "fmt" + "text/template" + + prommodel "github.com/prometheus/common/model" + promqlparser "github.com/prometheus/prometheus/promql/parser" +) + +// PromQLDialectValidator is the SLO flavour validator for prometheus backends dialect: PromQL. +const PromQLDialectValidator = promQLDialectValidator(false) + +type promQLDialectValidator bool + +func (promQLDialectValidator) ValidateLabelKey(k string) error { + if k == prommodel.MetricNameLabel { + return fmt.Errorf("the label key %q is not allowed", prommodel.MetricNameLabel) + } + if !prommodel.LabelName(k).IsValid() { + return fmt.Errorf("the label key %q is not valid", k) + } + + return nil +} + +func (promQLDialectValidator) ValidateLabelValue(k string) error { + if k == "" { + return fmt.Errorf("the label value is required") + } + + if !prommodel.LabelValue(k).IsValid() { + return fmt.Errorf("the label value %q is not valid", k) + } + + return nil +} + +func (promQLDialectValidator) ValidateAnnotationKey(k string) error { + if !prommodel.LabelName(k).IsValid() { + return fmt.Errorf("the annotation key %q is not valid", k) + } + + return nil +} + +func (promQLDialectValidator) ValidateAnnotationValue(k string) error { + if k == "" { + return fmt.Errorf("the annotation value is required") + } + + return nil +} + +var promExprTplAllowedFakeData = map[string]string{"window": "1m"} + +func (promQLDialectValidator) ValidateQueryExpression(queryExpression string) error { + if queryExpression == "" { + return fmt.Errorf("query is required") + } + + // The expressions set by users can have some allowed templated data. + // We are rendering the expression with fake data so prometheus can + // have a final expr and check if is correct. + tpl, err := template.New("expr").Parse(queryExpression) + if err != nil { + return err + } + + var tplB bytes.Buffer + err = tpl.Execute(&tplB, promExprTplAllowedFakeData) + if err != nil { + return err + } + + _, err = promqlparser.ParseExpr(tplB.String()) + + return err +} diff --git a/pkg/common/validation/slo.go b/pkg/common/validation/slo.go new file mode 100644 index 00000000..1396789c --- /dev/null +++ b/pkg/common/validation/slo.go @@ -0,0 +1,191 @@ +package validation + +import ( + "fmt" + + "github.com/slok/sloth/pkg/common/conventions" + commonerrors "github.com/slok/sloth/pkg/common/errors" + "github.com/slok/sloth/pkg/common/model" +) + +func isValidName(name string) error { + if name == "" { + return commonerrors.ErrRequired + } + + if !conventions.NameRegexp.MatchString(name) { + return fmt.Errorf("name must start and end with an alphanumeric and can only contain alphanumeric, '.', '_', and '-'") + } + + return nil +} + +func isValidSLIQueryTemplate(sliQuery string) error { + if sliQuery == "" { + return fmt.Errorf("query template is required: %w", commonerrors.ErrRequired) + } + + if !conventions.TplWindowRegex.MatchString(sliQuery) { + return fmt.Errorf("template must contain the {{ .window }} variable") + } + + return nil +} + +func isValidSLOSLI(slo model.PromSLO, dialect SLODialectValidator) error { + sli := slo.SLI + + if sli.Events == nil && sli.Raw == nil { + return fmt.Errorf("at least one SLI type is required") + } + + if sli.Events != nil && sli.Raw != nil { + return fmt.Errorf("only one SLI type is allowed") + } + + switch { + case sli.Events != nil: + // If they are the same, they are invalid. + if sli.Events.ErrorQuery == sli.Events.TotalQuery { + return fmt.Errorf("both error and total queries can't be the same") + } + + if err := isValidSLIQueryTemplate(sli.Events.ErrorQuery); err != nil { + return fmt.Errorf("sli error query template: %w", err) + } + + if err := isValidSLIQueryTemplate(sli.Events.TotalQuery); err != nil { + return fmt.Errorf("sli total query template: %w", err) + } + + if err := dialect.ValidateQueryExpression(sli.Events.ErrorQuery); err != nil { + return fmt.Errorf("sli error query expression: %w", err) + } + + if err := dialect.ValidateQueryExpression(sli.Events.TotalQuery); err != nil { + return fmt.Errorf("sli total query expression: %w", err) + } + + case sli.Raw != nil: + if err := isValidSLIQueryTemplate(sli.Raw.ErrorRatioQuery); err != nil { + return fmt.Errorf("sli raw query template: %w", err) + } + + if err := dialect.ValidateQueryExpression(sli.Raw.ErrorRatioQuery); err != nil { + return fmt.Errorf("sli raw query expression: %w", err) + } + } + + return nil +} + +func isValidSLOAlert(slo model.PromSLO, dialect SLODialectValidator) error { + if err := isValidAlert(slo.PageAlertMeta, dialect); err != nil { + return fmt.Errorf("page alert: %w", err) + } + + if err := isValidAlert(slo.TicketAlertMeta, dialect); err != nil { + return fmt.Errorf("ticket alert: %w", err) + } + + return nil +} + +func isValidAlert(alert model.PromAlertMeta, dialect SLODialectValidator) error { + if alert.Disable { + return nil + } + + if alert.Name == "" { + return fmt.Errorf("alert name is required") + } + + for k, v := range alert.Labels { + if err := dialect.ValidateLabelKey(k); err != nil { + return fmt.Errorf("invalid alert label key %q: %w", k, err) + } + if err := dialect.ValidateLabelValue(v); err != nil { + return fmt.Errorf("invalid alert label value %q: %w", v, err) + } + } + + for k, v := range alert.Annotations { + if err := dialect.ValidateAnnotationKey(k); err != nil { + return fmt.Errorf("invalid alert annotation key %q: %w", k, err) + } + if err := dialect.ValidateAnnotationValue(v); err != nil { + return fmt.Errorf("invalid alert annotation value %q: %w", v, err) + } + } + + return nil +} + +func isValidPlugins(slo model.PromSLO) error { + if slo.Plugins.OverrideDefaultPlugins && len(slo.Plugins.Plugins) == 0 { + return fmt.Errorf("override default plugins is set but no plugins are defined") + } + + for _, p := range slo.Plugins.Plugins { + if p.ID == "" { + return fmt.Errorf("plugin ID is required") + } + } + + return nil +} + +// SLODialectValidator is the interface that all SLO dialects must implement to validate +// SLOs. A dialect can me Prometheus PromQL, or VictoriaMetrics metricsQL for example. +type SLODialectValidator interface { + ValidateLabelKey(k string) error + ValidateLabelValue(k string) error + ValidateAnnotationKey(k string) error + ValidateAnnotationValue(k string) error + ValidateQueryExpression(queryExpression string) error +} + +func ValidateSLO(slo model.PromSLO, dialect SLODialectValidator) error { + if err := isValidName(slo.ID); err != nil { + return fmt.Errorf("invalid SLO ID: %w", err) + } + + if err := isValidName(slo.Name); err != nil { + return fmt.Errorf("invalid SLO name: %w", err) + } + + if err := isValidName(slo.Service); err != nil { + return fmt.Errorf("invalid SLO service: %w", err) + } + + if slo.TimeWindow == 0 { + return fmt.Errorf("time window is required") + } + + if slo.Objective <= 0 || slo.Objective > 100 { + return fmt.Errorf("objective must >0 and <=100") + } + + for k, v := range slo.Labels { + if err := dialect.ValidateLabelKey(k); err != nil { + return fmt.Errorf("invalid SLO label key %q: %w", k, err) + } + if err := dialect.ValidateLabelValue(v); err != nil { + return fmt.Errorf("invalid SLO label value %q: %w", v, err) + } + } + + if err := isValidSLOSLI(slo, dialect); err != nil { + return fmt.Errorf("invalid SLI: %w", err) + } + + if err := isValidSLOAlert(slo, dialect); err != nil { + return fmt.Errorf("invalid alert: %w", err) + } + + if err := isValidPlugins(slo); err != nil { + return fmt.Errorf("invalid plugins: %w", err) + } + + return nil +} diff --git a/pkg/common/model/slo_prometheus_test.go b/pkg/common/validation/slo_test.go similarity index 58% rename from pkg/common/model/slo_prometheus_test.go rename to pkg/common/validation/slo_test.go index d711077b..5037291e 100644 --- a/pkg/common/model/slo_prometheus_test.go +++ b/pkg/common/validation/slo_test.go @@ -1,4 +1,4 @@ -package model_test +package validation_test import ( "testing" @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/slok/sloth/pkg/common/model" + "github.com/slok/sloth/pkg/common/validation" ) func getGoodSLO() model.PromSLO { @@ -55,7 +56,7 @@ func getGoodSLO() model.PromSLO { } } -func TestModelValidationSpec(t *testing.T) { +func TestModelValidationSpecForPrometheusBackend(t *testing.T) { tests := map[string]struct { slo func() model.PromSLO expErrMessage string @@ -72,7 +73,7 @@ func TestModelValidationSpec(t *testing.T) { s.ID = "" return s }, - expErrMessage: "Key: 'PromSLO.ID' Error:Field validation for 'ID' failed on the 'required' tag", + expErrMessage: `invalid SLO ID: required`, }, "SLO ID must be alphanumeric, `.`, '_', and '-'.": { @@ -81,7 +82,7 @@ func TestModelValidationSpec(t *testing.T) { s.ID = "this-is-{a-test" return s }, - expErrMessage: "Key: 'PromSLO.ID' Error:Field validation for 'ID' failed on the 'name' tag", + expErrMessage: `invalid SLO ID: name must start and end with an alphanumeric and can only contain alphanumeric, '.', '_', and '-'`, }, "SLO ID must start with aphanumeric.": { @@ -90,7 +91,7 @@ func TestModelValidationSpec(t *testing.T) { s.ID = "_" + s.ID return s }, - expErrMessage: "Key: 'PromSLO.ID' Error:Field validation for 'ID' failed on the 'name' tag", + expErrMessage: `invalid SLO ID: name must start and end with an alphanumeric and can only contain alphanumeric, '.', '_', and '-'`, }, "SLO ID must end with aphanumeric.": { @@ -99,7 +100,7 @@ func TestModelValidationSpec(t *testing.T) { s.ID = s.ID + "_" return s }, - expErrMessage: "Key: 'PromSLO.ID' Error:Field validation for 'ID' failed on the 'name' tag", + expErrMessage: `invalid SLO ID: name must start and end with an alphanumeric and can only contain alphanumeric, '.', '_', and '-'`, }, "SLO Name is required.": { @@ -108,7 +109,7 @@ func TestModelValidationSpec(t *testing.T) { s.Name = "" return s }, - expErrMessage: "Key: 'PromSLO.Name' Error:Field validation for 'Name' failed on the 'required' tag", + expErrMessage: `invalid SLO name: required`, }, "SLO Name must be alphanumeric, `.`, '_', and '-'.": { @@ -117,7 +118,7 @@ func TestModelValidationSpec(t *testing.T) { s.Name = "this-is-{a-test" return s }, - expErrMessage: "Key: 'PromSLO.Name' Error:Field validation for 'Name' failed on the 'name' tag", + expErrMessage: `invalid SLO name: name must start and end with an alphanumeric and can only contain alphanumeric, '.', '_', and '-'`, }, "SLO Name must start with aphanumeric.": { @@ -126,7 +127,7 @@ func TestModelValidationSpec(t *testing.T) { s.Name = "_" + s.Name return s }, - expErrMessage: "Key: 'PromSLO.Name' Error:Field validation for 'Name' failed on the 'name' tag", + expErrMessage: `invalid SLO name: name must start and end with an alphanumeric and can only contain alphanumeric, '.', '_', and '-'`, }, "SLO Name must end with aphanumeric.": { @@ -135,7 +136,7 @@ func TestModelValidationSpec(t *testing.T) { s.Name = s.Name + "_" return s }, - expErrMessage: "Key: 'PromSLO.Name' Error:Field validation for 'Name' failed on the 'name' tag", + expErrMessage: `invalid SLO name: name must start and end with an alphanumeric and can only contain alphanumeric, '.', '_', and '-'`, }, "SLO Service is required.": { @@ -144,7 +145,7 @@ func TestModelValidationSpec(t *testing.T) { s.Service = "" return s }, - expErrMessage: "Key: 'PromSLO.Service' Error:Field validation for 'Service' failed on the 'required' tag", + expErrMessage: `invalid SLO service: required`, }, "SLO Service must be alphanumeric, `.`, '_', and '-'.": { @@ -153,7 +154,7 @@ func TestModelValidationSpec(t *testing.T) { s.Service = "this-is-{a-test" return s }, - expErrMessage: "Key: 'PromSLO.Service' Error:Field validation for 'Service' failed on the 'name' tag", + expErrMessage: `invalid SLO service: name must start and end with an alphanumeric and can only contain alphanumeric, '.', '_', and '-'`, }, "SLO Service must start with aphanumeric.": { @@ -162,7 +163,7 @@ func TestModelValidationSpec(t *testing.T) { s.Service = "_" + s.Service return s }, - expErrMessage: "Key: 'PromSLO.Service' Error:Field validation for 'Service' failed on the 'name' tag", + expErrMessage: `invalid SLO service: name must start and end with an alphanumeric and can only contain alphanumeric, '.', '_', and '-'`, }, "SLO Service must end with aphanumeric.": { @@ -171,7 +172,7 @@ func TestModelValidationSpec(t *testing.T) { s.Service = s.Service + "_" return s }, - expErrMessage: "Key: 'PromSLO.Service' Error:Field validation for 'Service' failed on the 'name' tag", + expErrMessage: `invalid SLO service: name must start and end with an alphanumeric and can only contain alphanumeric, '.', '_', and '-'`, }, "SLO without SLI type should fail.": { @@ -180,7 +181,7 @@ func TestModelValidationSpec(t *testing.T) { s.SLI = model.PromSLI{} return s }, - expErrMessage: "Key: 'PromSLO.SLI.' Error:Field validation for '' failed on the 'sli_type_required' tag", + expErrMessage: `invalid SLI: at least one SLI type is required`, }, "SLO with more than one SLI type should fail.": { @@ -191,7 +192,7 @@ func TestModelValidationSpec(t *testing.T) { } return s }, - expErrMessage: "Key: 'PromSLO.SLI.' Error:Field validation for '' failed on the 'one_sli_type' tag", + expErrMessage: `invalid SLI: only one SLI type is allowed`, }, "SLO SLI event queries must be different.": { @@ -201,16 +202,25 @@ func TestModelValidationSpec(t *testing.T) { s.SLI.Events.ErrorQuery = `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))` return s }, - expErrMessage: "Key: 'PromSLO.SLI.Events.' Error:Field validation for '' failed on the 'sli_events_queries_different' tag", + expErrMessage: `invalid SLI: both error and total queries can't be the same`, + }, + + "SLO SLI errir query should have a query.": { + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI.Events.ErrorQuery = "" + return s + }, + expErrMessage: `invalid SLI: sli error query template: query template is required: required`, }, "SLO SLI error query should be valid Prometheus expr.": { slo: func() model.PromSLO { s := getGoodSLO() - s.SLI.Events.ErrorQuery = "sum(rate(grpc_server_handled_requests_count{[1m]))" + s.SLI.Events.ErrorQuery = "sum(rate(grpc_server_handled_requests_count{[{{ .window }}]))" return s }, - expErrMessage: "Key: 'PromSLO.SLI.Events.ErrorQuery' Error:Field validation for 'ErrorQuery' failed on the 'prom_expr' tag", + expErrMessage: `invalid SLI: sli error query expression: 1:45: parse error: unexpected character inside braces: '['`, }, "SLO SLI error query should have required template vars.": { @@ -219,16 +229,25 @@ func TestModelValidationSpec(t *testing.T) { s.SLI.Events.ErrorQuery = "sum(rate(grpc_server_handled_requests_count[1m]))" return s }, - expErrMessage: "Key: 'PromSLO.SLI.Events.ErrorQuery' Error:Field validation for 'ErrorQuery' failed on the 'template_vars' tag", + expErrMessage: `invalid SLI: sli error query template: template must contain the {{ .window }} variable`, + }, + + "SLO SLI total query should have a query.": { + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI.Events.TotalQuery = "" + return s + }, + expErrMessage: `invalid SLI: sli total query template: query template is required: required`, }, "SLO SLI total query should be valid Prometheus expr.": { slo: func() model.PromSLO { s := getGoodSLO() - s.SLI.Events.TotalQuery = "sum(rate(grpc_server_handled_requests_count{[1m]))" + s.SLI.Events.TotalQuery = "sum(rate(grpc_server_handled_requests_count{[{{ .window }}]))" return s }, - expErrMessage: "Key: 'PromSLO.SLI.Events.TotalQuery' Error:Field validation for 'TotalQuery' failed on the 'prom_expr' tag", + expErrMessage: `invalid SLI: sli total query expression: 1:45: parse error: unexpected character inside braces: '['`, }, "SLO SLI total query should have required template vars.": { @@ -237,7 +256,50 @@ func TestModelValidationSpec(t *testing.T) { s.SLI.Events.TotalQuery = "sum(rate(grpc_server_handled_requests_count[1m]))" return s }, - expErrMessage: "Key: 'PromSLO.SLI.Events.TotalQuery' Error:Field validation for 'TotalQuery' failed on the 'template_vars' tag", + expErrMessage: `invalid SLI: sli total query template: template must contain the {{ .window }} variable`, + }, + + "SLO SLI raw query should have a query.": { + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI.Events = nil + s.SLI.Raw = &model.PromSLIRaw{} + return s + }, + expErrMessage: `invalid SLI: sli raw query template: query template is required: required`, + }, + + "SLO SLI raw query should have required template vars.": { + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI.Events = nil + s.SLI.Raw = &model.PromSLIRaw{ + ErrorRatioQuery: "sum(rate(grpc_server_handled_requests_count[1m]))", + } + return s + }, + expErrMessage: `invalid SLI: sli raw query template: template must contain the {{ .window }} variable`, + }, + + "SLO SLI raw query should be valid Prometheus expr.": { + slo: func() model.PromSLO { + s := getGoodSLO() + s.SLI.Events = nil + s.SLI.Raw = &model.PromSLIRaw{ + ErrorRatioQuery: "sum(rate(grpc_server_handled_requests_count{[{{ .window }}]))", + } + return s + }, + expErrMessage: `invalid SLI: sli raw query expression: 1:45: parse error: unexpected character inside braces: '['`, + }, + + "SLO time window should be set.": { + slo: func() model.PromSLO { + s := getGoodSLO() + s.TimeWindow = 0 + return s + }, + expErrMessage: `time window is required`, }, "SLO Objective shouldn't be less than 0.": { @@ -246,7 +308,7 @@ func TestModelValidationSpec(t *testing.T) { s.Objective = -1 return s }, - expErrMessage: "Key: 'PromSLO.Objective' Error:Field validation for 'Objective' failed on the 'gt' tag", + expErrMessage: `objective must >0 and <=100`, }, "SLO Objective shouldn't be 0.": { @@ -255,7 +317,7 @@ func TestModelValidationSpec(t *testing.T) { s.Objective = 0 return s }, - expErrMessage: "Key: 'PromSLO.Objective' Error:Field validation for 'Objective' failed on the 'gt' tag", + expErrMessage: `objective must >0 and <=100`, }, "SLO Objective shouldn't be greater than 100.": { @@ -264,7 +326,7 @@ func TestModelValidationSpec(t *testing.T) { s.Objective = 100.0001 return s }, - expErrMessage: "Key: 'PromSLO.Objective' Error:Field validation for 'Objective' failed on the 'lte' tag", + expErrMessage: `objective must >0 and <=100`, }, "SLO Labels should be valid prometheus keys.": { @@ -273,7 +335,7 @@ func TestModelValidationSpec(t *testing.T) { s.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'PromSLO.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", + expErrMessage: `invalid SLO label key "\xf0\x8f\xbf\xbf": the label key "\xf0\x8f\xbf\xbf" is not valid`, }, "SLO Labels should have prometheus values.": { @@ -282,7 +344,7 @@ func TestModelValidationSpec(t *testing.T) { s.Labels["something"] = "" return s }, - expErrMessage: "Key: 'PromSLO.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", + expErrMessage: `invalid SLO label value "": the label value is required`, }, "SLO Labels should be valid prometheus values.": { @@ -291,7 +353,7 @@ func TestModelValidationSpec(t *testing.T) { s.Labels["something"] = "\xc3\x28" return s }, - expErrMessage: "Key: 'PromSLO.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", + expErrMessage: `invalid SLO label value "\xc3(": the label value "\xc3(" is not valid`, }, "SLO page alert name is required.": { @@ -300,7 +362,7 @@ func TestModelValidationSpec(t *testing.T) { s.PageAlertMeta.Name = "" return s }, - expErrMessage: "Key: 'PromSLO.PageAlertMeta.Name' Error:Field validation for 'Name' failed on the 'required_if_enabled' tag", + expErrMessage: `invalid alert: page alert: alert name is required`, }, "SLO page alert fields are not required if disabled .": { @@ -320,7 +382,7 @@ func TestModelValidationSpec(t *testing.T) { s.TicketAlertMeta.Name = "" return s }, - expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Name' Error:Field validation for 'Name' failed on the 'required_if_enabled' tag", + expErrMessage: `invalid alert: ticket alert: alert name is required`, }, "SLO warning alert fields are not required if disabled .": { @@ -340,7 +402,7 @@ func TestModelValidationSpec(t *testing.T) { s.PageAlertMeta.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'PromSLO.PageAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", + expErrMessage: `invalid alert: page alert: invalid alert label key "\xf0\x8f\xbf\xbf": the label key "\xf0\x8f\xbf\xbf" is not valid`, }, "SLO page alert labels should have prometheus values.": { @@ -349,7 +411,7 @@ func TestModelValidationSpec(t *testing.T) { s.PageAlertMeta.Labels["something"] = "" return s }, - expErrMessage: "Key: 'PromSLO.PageAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", + expErrMessage: `invalid alert: page alert: invalid alert label value "": the label value is required`, }, "SLO page alert labels should be valid prometheus values.": { @@ -358,7 +420,7 @@ func TestModelValidationSpec(t *testing.T) { s.PageAlertMeta.Labels["something"] = "\xc3\x28" return s }, - expErrMessage: "Key: 'PromSLO.PageAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", + expErrMessage: `invalid alert: page alert: invalid alert label value "\xc3(": the label value "\xc3(" is not valid`, }, "SLO page alert annotations should be valid prometheus keys.": { @@ -367,7 +429,7 @@ func TestModelValidationSpec(t *testing.T) { s.PageAlertMeta.Annotations["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'PromSLO.PageAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", + expErrMessage: `invalid alert: page alert: invalid alert annotation key "\xf0\x8f\xbf\xbf": the annotation key "\xf0\x8f\xbf\xbf" is not valid`, }, "SLO page alert annotations should have prometheus values.": { @@ -376,7 +438,7 @@ func TestModelValidationSpec(t *testing.T) { s.PageAlertMeta.Annotations["something"] = "" return s }, - expErrMessage: "Key: 'PromSLO.PageAlertMeta.Annotations[something]' Error:Field validation for 'Annotations[something]' failed on the 'required' tag", + expErrMessage: `invalid alert: page alert: invalid alert annotation value "": the annotation value is required`, }, "SLO warning alert labels should be valid prometheus keys.": { @@ -385,7 +447,7 @@ func TestModelValidationSpec(t *testing.T) { s.TicketAlertMeta.Labels["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Labels[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Labels[\xF0\x8F\xBF\xBF]' failed on the 'prom_label_key' tag", + expErrMessage: `invalid alert: ticket alert: invalid alert label key "\xf0\x8f\xbf\xbf": the label key "\xf0\x8f\xbf\xbf" is not valid`, }, "SLO warning alert labels should have prometheus values.": { @@ -394,7 +456,7 @@ func TestModelValidationSpec(t *testing.T) { s.TicketAlertMeta.Labels["something"] = "" return s }, - expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'required' tag", + expErrMessage: `invalid alert: ticket alert: invalid alert label value "": the label value is required`, }, "SLO warning alert labels should be valid prometheus values.": { @@ -403,7 +465,7 @@ func TestModelValidationSpec(t *testing.T) { s.TicketAlertMeta.Labels["something"] = "\xc3\x28" return s }, - expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Labels[something]' Error:Field validation for 'Labels[something]' failed on the 'prom_label_value' tag", + expErrMessage: `invalid alert: ticket alert: invalid alert label value "\xc3(": the label value "\xc3(" is not valid`, }, "SLO warning alert annotations should be valid prometheus keys.": { @@ -412,7 +474,7 @@ func TestModelValidationSpec(t *testing.T) { s.TicketAlertMeta.Annotations["\xF0\x8F\xBF\xBF"] = "label key is wrong" return s }, - expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Annotations[\xF0\x8F\xBF\xBF]' Error:Field validation for 'Annotations[\xF0\x8F\xBF\xBF]' failed on the 'prom_annot_key' tag", + expErrMessage: `invalid alert: ticket alert: invalid alert annotation key "\xf0\x8f\xbf\xbf": the annotation key "\xf0\x8f\xbf\xbf" is not valid`, }, "SLO warning alert annotations should have prometheus values.": { @@ -421,7 +483,28 @@ func TestModelValidationSpec(t *testing.T) { s.TicketAlertMeta.Annotations["something"] = "" return s }, - expErrMessage: "Key: 'PromSLO.TicketAlertMeta.Annotations[something]' Error:Field validation for 'Annotations[something]' failed on the 'required' tag", + expErrMessage: `invalid alert: ticket alert: invalid alert annotation value "": the annotation value is required`, + }, + + "SLO plugins should have ID.": { + slo: func() model.PromSLO { + s := getGoodSLO() + s.Plugins.Plugins = []model.PromSLOPluginMetadata{ + {ID: ""}, + } + return s + }, + expErrMessage: `invalid plugins: plugin ID is required`, + }, + + "SLO plugins should be declared if override default plugins is used.": { + slo: func() model.PromSLO { + s := getGoodSLO() + s.Plugins.OverrideDefaultPlugins = true + s.Plugins.Plugins = []model.PromSLOPluginMetadata{} + return s + }, + expErrMessage: `invalid plugins: override default plugins is set but no plugins are defined`, }, } @@ -430,7 +513,7 @@ func TestModelValidationSpec(t *testing.T) { assert := assert.New(t) slo := test.slo() - err := slo.Validate() + err := validation.ValidateSLO(slo, validation.PromQLDialectValidator) if test.expErrMessage != "" { assert.Error(err) From 2d8ba83a228b60a9e40ba1d7ae05f131ebcdfb42 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sun, 20 Apr 2025 09:00:24 +0200 Subject: [PATCH 055/173] Remove not optimized SLI rule generation flag Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 7 +-- cmd/sloth/commands/generate.go | 50 +++++++++---------- cmd/sloth/commands/k8scontroller.go | 36 +++++++------ internal/app/generate/generate.go | 2 +- .../plugin/slo/core/sli_rules_v1/plugin.go | 4 +- .../slo/core/sli_rules_v1/plugin_test.go | 8 ++- 6 files changed, 50 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b026a6eb..e0154ae4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Core SLO validation and SLO rules generation migrated to SLO plugins. - (BREAKING) `--sli-plugins-path`, `--slo-plugins-path`, `-m` args and it's env vars `SLOTH_SLI_PLUGINS_PATH`and `SLOTH_SLO_PLUGINS_PATH` have been removed in favor or `--plugins-path`, `-p` and it's env var `SLOTH_PLUGINS_PATH` that discovers and loads SLI and SLO plugins with a single flag. - Simplify validation and improve validation message by using custom logic instead of `go-playground/validator`. +- (BREAKING) `--disable-optimized-rules` flag and associated env var has been removed. ### Added @@ -16,11 +17,11 @@ - Sloth conventions can be imported in Go apps using `github.com/slok/sloth/pkg/common/conventions`. - Sloth SLO validation logic can be imported in Go apps using `github.com/slok/sloth/pkg/common/validation`. - A new SLO rule generation plugin system has been added to be able to change/extend the SLO rule generation process. -- SLO plugins can be loaded from FS directories recursively using `--slo-plugins-path` in the commands. -- SLO plugins have a priority value to be able to order in the execution chain. +- SLO plugins can be loaded from FS directories recursively using `--plugins-path` in the commands. +- SLO plugins have a `priority` value to be able to order in the execution chain. - Sloth regular (non-k8s) `prometheus/v1` API support for SLO plugins at SLO group level and per SLO level. - Sloth K8s CRD `sloth.slok.dev/v1/PrometheusServiceLevel` API support for SLO plugins at SLO group level and per SLO level. -- Allow overriding previous declared plugins (includes defaults) at SLO group and SLO level. +- Allow overriding previous declared SLO plugins (includes defaults) at SLO group and SLO level. ## [v0.12.0] - 2025-03-27 diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 1a3aa750..10fb156f 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -33,17 +33,16 @@ import ( ) type generateCommand struct { - slosInput string - slosOut string - slosExcludeRegex string - slosIncludeRegex string - disableRecordings bool - disableAlerts bool - disableOptimizedRules bool - extraLabels map[string]string - pluginsPaths []string - sloPeriodWindowsPath string - sloPeriod string + slosInput string + slosOut string + slosExcludeRegex string + slosIncludeRegex string + disableRecordings bool + disableAlerts bool + extraLabels map[string]string + pluginsPaths []string + sloPeriodWindowsPath string + sloPeriod string } // NewGenerateCommand returns the generate command. @@ -61,7 +60,6 @@ func NewGenerateCommand(app *kingpin.Application) Command { cmd.Flag("plugins-path", "The path to SLI and SLO plugins (can be repeated).").Short('p').StringsVar(&c.pluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) - cmd.Flag("disable-optimized-rules", "If enabled it will disable optimized generated rules.").BoolVar(&c.disableOptimizedRules) return c } @@ -244,13 +242,12 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { } gen := generator{ - logger: logger, - windowsRepo: windowsRepo, - disableRecordings: g.disableRecordings, - disableAlerts: g.disableAlerts, - disableOptimizedRules: g.disableOptimizedRules, - extraLabels: g.extraLabels, - sloPluginRepo: pluginsRepo, + logger: logger, + windowsRepo: windowsRepo, + disableRecordings: g.disableRecordings, + disableAlerts: g.disableAlerts, + extraLabels: g.extraLabels, + sloPluginRepo: pluginsRepo, } for _, genTarget := range genTargets { @@ -305,13 +302,12 @@ type generateTarget struct { } type generator struct { - logger log.Logger - windowsRepo alert.WindowsRepo - disableRecordings bool - disableAlerts bool - disableOptimizedRules bool - extraLabels map[string]string - sloPluginRepo *storagefs.FilePluginRepo + logger log.Logger + windowsRepo alert.WindowsRepo + disableRecordings bool + disableAlerts bool + extraLabels map[string]string + sloPluginRepo *storagefs.FilePluginRepo } // GeneratePrometheus generates the SLOs based on a raw regular Prometheus spec format input and outs a Prometheus raw yaml. @@ -426,7 +422,7 @@ func (g generator) generateRules(ctx context.Context, info model.Info, slos mode sliPlugin, err := generate.NewSLOProcessorFromSLOPluginV1( plugincoreslirulesv1.NewPlugin, g.logger.WithValues(log.Kv{"plugin": plugincoreslirulesv1.PluginID}), - plugincoreslirulesv1.PluginConfig{Optimized: !g.disableOptimizedRules}, + plugincoreslirulesv1.PluginConfig{}, ) if err != nil { return nil, fmt.Errorf("could not create SLI rules plugin: %w", err) diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index 087d37ca..3dc048ba 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -56,23 +56,22 @@ const ( ) type kubeControllerCommand struct { - extraLabels map[string]string - workers int - kubeConfig string - kubeContext string - resyncInterval time.Duration - namespace string - labelSelector string - kubeLocal bool - runMode string - metricsPath string - hotReloadPath string - hotReloadAddr string - metricsListenAddr string - pluginsPaths []string - sloPeriodWindowsPath string - sloPeriod string - disableOptimizedRules bool + extraLabels map[string]string + workers int + kubeConfig string + kubeContext string + resyncInterval time.Duration + namespace string + labelSelector string + kubeLocal bool + runMode string + metricsPath string + hotReloadPath string + hotReloadAddr string + metricsListenAddr string + pluginsPaths []string + sloPeriodWindowsPath string + sloPeriod string } // NewKubeControllerCommand returns the Kubernetes controller command. @@ -99,7 +98,6 @@ func NewKubeControllerCommand(app *kingpin.Application) Command { cmd.Flag("plugins-path", "The path to SLI and SLO plugins (can be repeated).").Short('p').StringsVar(&c.pluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) - cmd.Flag("disable-optimized-rules", "If enabled it will disable optimized generated rules.").BoolVar(&c.disableOptimizedRules) return c } @@ -305,7 +303,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error sliRuleGen, err := generate.NewSLOProcessorFromSLOPluginV1( plugincoreslirulesv1.NewPlugin, logger.WithValues(log.Kv{"plugin": plugincoreslirulesv1.PluginID}), - plugincoreslirulesv1.PluginConfig{Optimized: !k.disableOptimizedRules}, + plugincoreslirulesv1.PluginConfig{}, ) if err != nil { return fmt.Errorf("could not create SLI rules plugin: %w", err) diff --git a/internal/app/generate/generate.go b/internal/app/generate/generate.go index 00ae81c3..63583c87 100644 --- a/internal/app/generate/generate.go +++ b/internal/app/generate/generate.go @@ -64,7 +64,7 @@ func (c *ServiceConfig) defaults() error { plugin, err := NewSLOProcessorFromSLOPluginV1( plugincoreslirulesv1.NewPlugin, c.Logger.WithValues(log.Kv{"plugin": plugincoreslirulesv1.PluginID}), - plugincoreslirulesv1.PluginConfig{Optimized: true}, + plugincoreslirulesv1.PluginConfig{}, ) if err != nil { return fmt.Errorf("could not create SLI rules plugin: %w", err) diff --git a/internal/plugin/slo/core/sli_rules_v1/plugin.go b/internal/plugin/slo/core/sli_rules_v1/plugin.go index cbe68f04..c41dfc0a 100644 --- a/internal/plugin/slo/core/sli_rules_v1/plugin.go +++ b/internal/plugin/slo/core/sli_rules_v1/plugin.go @@ -23,7 +23,7 @@ const ( ) type PluginConfig struct { - Optimized bool + DisableOptimized bool `json:"disableOptimized,omitempty"` } func NewPlugin(c json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { @@ -42,7 +42,7 @@ type plugin struct { func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { genFunc := factorySLIRecordGenerator - if p.cfg.Optimized { + if !p.cfg.DisableOptimized { genFunc = optimizedFactorySLIRecordGenerator } diff --git a/internal/plugin/slo/core/sli_rules_v1/plugin_test.go b/internal/plugin/slo/core/sli_rules_v1/plugin_test.go index 1ecafd3b..d5f00d97 100644 --- a/internal/plugin/slo/core/sli_rules_v1/plugin_test.go +++ b/internal/plugin/slo/core/sli_rules_v1/plugin_test.go @@ -497,7 +497,7 @@ func TestGenerateSLIRecordingRules(t *testing.T) { require := require.New(t) // Load plugin - config, _ := json.Marshal(map[string]any{"optimized": test.optimized}) + config, _ := json.Marshal(map[string]any{"disableOptimized": !test.optimized}) plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: config}) require.NoError(err) @@ -520,9 +520,8 @@ func TestGenerateSLIRecordingRules(t *testing.T) { } func BenchmarkPluginYaegi(b *testing.B) { - config, _ := json.Marshal(map[string]any{"optimized": true}) plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{ - PluginConfiguration: config, + PluginConfiguration: []byte("{}"), }) if err != nil { b.Fatal(err) @@ -541,8 +540,7 @@ func BenchmarkPluginYaegi(b *testing.B) { } func BenchmarkPluginGo(b *testing.B) { - config, _ := json.Marshal(map[string]any{"optimized": true}) - plugin, err := plugin.NewPlugin(config, pluginslov1.AppUtils{}) + plugin, err := plugin.NewPlugin([]byte("{}"), pluginslov1.AppUtils{}) if err != nil { b.Fatal(err) } From 34056d020afa9034a4fc8355b71d0046b12f2775 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Wed, 30 Apr 2025 19:17:06 +0200 Subject: [PATCH 056/173] Pass default plugins as a chain Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 16 ++++--- cmd/sloth/commands/k8scontroller.go | 16 ++++--- internal/app/generate/generate.go | 69 +++++++++++++---------------- 3 files changed, 48 insertions(+), 53 deletions(-) diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 10fb156f..1fde8700 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -465,13 +465,15 @@ func (g generator) generateRules(ctx context.Context, info model.Info, slos mode // Generate. controller, err := generate.NewService(generate.ServiceConfig{ - AlertGenerator: alert.NewGenerator(g.windowsRepo), - SLIRulesGenSLOPlugin: sliRuleGen, - MetadataRulesGenSLOPlugin: metaRuleGen, - AlertRulesGenSLOPlugin: alertRuleGen, - ValidateSLOPlugin: validatePlugin, - SLOPluginGetter: g.sloPluginRepo, - Logger: g.logger, + AlertGenerator: alert.NewGenerator(g.windowsRepo), + DefaultPlugins: []generate.SLOProcessor{ + validatePlugin, + sliRuleGen, + metaRuleGen, + alertRuleGen, + }, + SLOPluginGetter: g.sloPluginRepo, + Logger: g.logger, }) if err != nil { return nil, fmt.Errorf("could not create application service: %w", err) diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index 3dc048ba..a2cc4b61 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -338,13 +338,15 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error // Create the generate app service (the one that the CLIs use). generator, err := generate.NewService(generate.ServiceConfig{ - AlertGenerator: alert.NewGenerator(windowsRepo), - SLIRulesGenSLOPlugin: sliRuleGen, - MetadataRulesGenSLOPlugin: metaRuleGen, - AlertRulesGenSLOPlugin: alertRuleGen, - ValidateSLOPlugin: validatePlugin, - SLOPluginGetter: pluginsRepo, - Logger: generatorLogger{Logger: logger}, + AlertGenerator: alert.NewGenerator(windowsRepo), + DefaultPlugins: []generate.SLOProcessor{ + validatePlugin, + sliRuleGen, + metaRuleGen, + alertRuleGen, + }, + SLOPluginGetter: pluginsRepo, + Logger: generatorLogger{Logger: logger}, }) if err != nil { return fmt.Errorf("could not create Prometheus rules generator: %w", err) diff --git a/internal/app/generate/generate.go b/internal/app/generate/generate.go index 63583c87..41cbbbfa 100644 --- a/internal/app/generate/generate.go +++ b/internal/app/generate/generate.go @@ -36,13 +36,10 @@ type SLOPluginGetter interface { // ServiceConfig is the application service configuration. type ServiceConfig struct { - AlertGenerator AlertGenerator - SLIRulesGenSLOPlugin SLOProcessor - AlertRulesGenSLOPlugin SLOProcessor - MetadataRulesGenSLOPlugin SLOProcessor - ValidateSLOPlugin SLOProcessor - SLOPluginGetter SLOPluginGetter - Logger log.Logger + AlertGenerator AlertGenerator + DefaultPlugins []SLOProcessor + SLOPluginGetter SLOPluginGetter + Logger log.Logger } func (c *ServiceConfig) defaults() error { @@ -60,8 +57,17 @@ func (c *ServiceConfig) defaults() error { c.Logger = c.Logger.WithValues(log.Kv{"svc": "generate.prometheus.Service"}) // Default plugins. - if c.SLIRulesGenSLOPlugin == nil { - plugin, err := NewSLOProcessorFromSLOPluginV1( + if c.DefaultPlugins == nil { + pluginValidate, err := NewSLOProcessorFromSLOPluginV1( + plugincorevalidatev1.NewPlugin, + c.Logger.WithValues(log.Kv{"plugin": plugincorevalidatev1.PluginID}), + nil, + ) + if err != nil { + return fmt.Errorf("could not create SLO validate plugin: %w", err) + } + + pluginSLI, err := NewSLOProcessorFromSLOPluginV1( plugincoreslirulesv1.NewPlugin, c.Logger.WithValues(log.Kv{"plugin": plugincoreslirulesv1.PluginID}), plugincoreslirulesv1.PluginConfig{}, @@ -69,21 +75,8 @@ func (c *ServiceConfig) defaults() error { if err != nil { return fmt.Errorf("could not create SLI rules plugin: %w", err) } - c.SLIRulesGenSLOPlugin = plugin - } - if c.AlertRulesGenSLOPlugin == nil { - plugin, err := NewSLOProcessorFromSLOPluginV1( - plugincorealertrulesv1.NewPlugin, - c.Logger.WithValues(log.Kv{"plugin": plugincorealertrulesv1.PluginID}), - nil, - ) - if err != nil { - return fmt.Errorf("could not create alert rules plugin: %w", err) - } - c.AlertRulesGenSLOPlugin = plugin - } - if c.MetadataRulesGenSLOPlugin == nil { - plugin, err := NewSLOProcessorFromSLOPluginV1( + + pluginMeta, err := NewSLOProcessorFromSLOPluginV1( plugincoremetadatarulesv1.NewPlugin, c.Logger.WithValues(log.Kv{"plugin": plugincoremetadatarulesv1.PluginID}), nil, @@ -91,19 +84,22 @@ func (c *ServiceConfig) defaults() error { if err != nil { return fmt.Errorf("could not create metadata rules plugin: %w", err) } - c.MetadataRulesGenSLOPlugin = plugin - } - if c.ValidateSLOPlugin == nil { - plugin, err := NewSLOProcessorFromSLOPluginV1( - plugincorevalidatev1.NewPlugin, - c.Logger.WithValues(log.Kv{"plugin": plugincorevalidatev1.PluginID}), + pluginAlert, err := NewSLOProcessorFromSLOPluginV1( + plugincorealertrulesv1.NewPlugin, + c.Logger.WithValues(log.Kv{"plugin": plugincorealertrulesv1.PluginID}), nil, ) if err != nil { - return fmt.Errorf("could not create SLO validate plugin: %w", err) + return fmt.Errorf("could not create alert rules plugin: %w", err) + } + + c.DefaultPlugins = []SLOProcessor{ + pluginValidate, + pluginSLI, + pluginMeta, + pluginAlert, } - c.ValidateSLOPlugin = plugin } return nil @@ -132,13 +128,8 @@ func NewService(config ServiceConfig) (*Service, error) { return &Service{ alertGen: config.AlertGenerator, sloPluginGetter: config.SLOPluginGetter, - defaultPlugins: []SLOProcessor{ - config.ValidateSLOPlugin, - config.SLIRulesGenSLOPlugin, - config.AlertRulesGenSLOPlugin, - config.MetadataRulesGenSLOPlugin, - }, - logger: config.Logger, + defaultPlugins: config.DefaultPlugins, + logger: config.Logger, }, nil } From 8c7c6d5da383cdfa42871bea7c25570212d2928e Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Thu, 1 May 2025 08:48:38 +0200 Subject: [PATCH 057/173] Support env vars on SLO plugins Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 2 ++ go.mod | 1 + go.sum | 2 ++ internal/pluginengine/slo/custom/custom.go | 3 ++ .../slo/custom/github_com-caarlos0-env-v11.go | 36 +++++++++++++++++++ internal/pluginengine/slo/slo.go | 2 +- 6 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 internal/pluginengine/slo/custom/github_com-caarlos0-env-v11.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e0154ae4..317ea31b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ - Sloth regular (non-k8s) `prometheus/v1` API support for SLO plugins at SLO group level and per SLO level. - Sloth K8s CRD `sloth.slok.dev/v1/PrometheusServiceLevel` API support for SLO plugins at SLO group level and per SLO level. - Allow overriding previous declared SLO plugins (includes defaults) at SLO group and SLO level. +- SLO plugins can access env vars and use OS/exec by default. +- Allow `github.com/caarlos0/env/v11` module in SLO plugins. ## [v0.12.0] - 2025-03-27 diff --git a/go.mod b/go.mod index c28cf82b..2254a2dd 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.0 require ( github.com/OpenSLO/oslo v0.12.0 github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/caarlos0/env/v11 v11.3.1 github.com/oklog/run v1.1.0 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.0 github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0 diff --git a/go.sum b/go.sum index c63558f6..fa1e75f2 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= 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/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/pluginengine/slo/custom/custom.go b/internal/pluginengine/slo/custom/custom.go index b00b14f0..19e04d41 100644 --- a/internal/pluginengine/slo/custom/custom.go +++ b/internal/pluginengine/slo/custom/custom.go @@ -2,10 +2,13 @@ package custom import ( "reflect" + + _ "github.com/caarlos0/env/v11" // Used only by yaegi plugins, not by Sloth. ) //go:generate yaegi extract --name custom github.com/prometheus/common/model github.com/prometheus/prometheus/model/rulefmt github.com/prometheus/prometheus/promql/parser //go:generate yaegi extract --name custom github.com/slok/sloth/pkg/prometheus/plugin/slo/v1 github.com/slok/sloth/pkg/common/conventions github.com/slok/sloth/pkg/common/model github.com/slok/sloth/pkg/common/utils/data github.com/slok/sloth/pkg/common/utils/prometheus github.com/slok/sloth/pkg/common/validation +//go:generate yaegi extract --name custom github.com/caarlos0/env/v11 // Symbols variable stores the map of custom Yaegi symbols per package. var Symbols = map[string]map[string]reflect.Value{} diff --git a/internal/pluginengine/slo/custom/github_com-caarlos0-env-v11.go b/internal/pluginengine/slo/custom/github_com-caarlos0-env-v11.go new file mode 100644 index 00000000..a8fc3039 --- /dev/null +++ b/internal/pluginengine/slo/custom/github_com-caarlos0-env-v11.go @@ -0,0 +1,36 @@ +// Code generated by 'yaegi extract github.com/caarlos0/env/v11'. DO NOT EDIT. + +package custom + +import ( + "github.com/caarlos0/env/v11" + "reflect" +) + +func init() { + Symbols["github.com/caarlos0/env/v11/env"] = map[string]reflect.Value{ + // function, constant and variable definitions + "GetFieldParams": reflect.ValueOf(env.GetFieldParams), + "GetFieldParamsWithOptions": reflect.ValueOf(env.GetFieldParamsWithOptions), + "Parse": reflect.ValueOf(env.Parse), + "ParseWithOptions": reflect.ValueOf(env.ParseWithOptions), + "ToMap": reflect.ValueOf(env.ToMap), + + // type definitions + "AggregateError": reflect.ValueOf((*env.AggregateError)(nil)), + "EmptyEnvVarError": reflect.ValueOf((*env.EmptyEnvVarError)(nil)), + "EmptyVarError": reflect.ValueOf((*env.EmptyVarError)(nil)), + "EnvVarIsNotSetError": reflect.ValueOf((*env.EnvVarIsNotSetError)(nil)), + "FieldParams": reflect.ValueOf((*env.FieldParams)(nil)), + "LoadFileContentError": reflect.ValueOf((*env.LoadFileContentError)(nil)), + "NoParserError": reflect.ValueOf((*env.NoParserError)(nil)), + "NoSupportedTagOptionError": reflect.ValueOf((*env.NoSupportedTagOptionError)(nil)), + "NotStructPtrError": reflect.ValueOf((*env.NotStructPtrError)(nil)), + "OnSetFn": reflect.ValueOf((*env.OnSetFn)(nil)), + "Options": reflect.ValueOf((*env.Options)(nil)), + "ParseError": reflect.ValueOf((*env.ParseError)(nil)), + "ParseValueError": reflect.ValueOf((*env.ParseValueError)(nil)), + "ParserFunc": reflect.ValueOf((*env.ParserFunc)(nil)), + "VarIsNotSetError": reflect.ValueOf((*env.VarIsNotSetError)(nil)), + } +} diff --git a/internal/pluginengine/slo/slo.go b/internal/pluginengine/slo/slo.go index a8e76b33..caceabe5 100644 --- a/internal/pluginengine/slo/slo.go +++ b/internal/pluginengine/slo/slo.go @@ -37,7 +37,7 @@ var packageRegexp = regexp.MustCompile(`(?m)^package +([^\s]+) *$`) func (p pluginLoader) LoadRawPlugin(ctx context.Context, src string) (*Plugin, error) { // Load the plugin in a new interpreter. // For each plugin we need to use an independent interpreter to avoid name collisions. - yaegiInterp, err := newYaeginInterpreter(false, false) + yaegiInterp, err := newYaeginInterpreter(true, true) if err != nil { return nil, fmt.Errorf("could not create a new Yaegi interpreter: %w", err) } From 15d236e85aef179a85b68d5c00e124dd0a21a071 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Fri, 2 May 2025 17:24:10 +0200 Subject: [PATCH 058/173] Allow SLO plugins at application level using cmd flags Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 1 + cmd/sloth/commands/generate.go | 11 ++++++++ cmd/sloth/commands/helpers.go | 27 ++++++++++++++++++ cmd/sloth/commands/k8scontroller.go | 9 ++++++ internal/app/generate/generate.go | 13 +++++++-- internal/app/generate/generate_test.go | 39 +++++++++++++++++++++----- internal/storage/io/k8s_sloth.go | 4 +-- internal/storage/io/k8s_sloth_test.go | 2 +- internal/storage/io/sloth.go | 4 +-- internal/storage/io/sloth_test.go | 2 +- pkg/common/model/slo_prometheus.go | 4 +-- pkg/common/validation/slo.go | 4 +-- pkg/common/validation/slo_test.go | 6 ++-- 13 files changed, 103 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 317ea31b..0378433a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Allow overriding previous declared SLO plugins (includes defaults) at SLO group and SLO level. - SLO plugins can access env vars and use OS/exec by default. - Allow `github.com/caarlos0/env/v11` module in SLO plugins. +- Add `--slo-plugins` and `-s` flag (`generate` and `k8s controller`) to be able to declare SLO plugins at cmd level, these plugins will be applied to all SLOs. ## [v0.12.0] - 2025-03-27 diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 1fde8700..a0d7040b 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -43,6 +43,7 @@ type generateCommand struct { pluginsPaths []string sloPeriodWindowsPath string sloPeriod string + sloPlugins []string } // NewGenerateCommand returns the generate command. @@ -60,6 +61,7 @@ func NewGenerateCommand(app *kingpin.Application) Command { cmd.Flag("plugins-path", "The path to SLI and SLO plugins (can be repeated).").Short('p').StringsVar(&c.pluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) + cmd.Flag("slo-plugins", `SLO plugins chain declaration in JSON format '{"id": "foo","priority": 0,"config": "{}"}' (Can be repeated).`).Short('s').StringsVar(&c.sloPlugins) return c } @@ -114,6 +116,12 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { return err } + // Load SLO plugin declarations at CMD level. + cmdLevelSLOPlugins, err := mapCmdPluginToModel(ctx, g.sloPlugins) + if err != nil { + return fmt.Errorf("could not load slo plugin declarations: %w", err) + } + // Windows repository. var wfs fs.FS if g.sloPeriodWindowsPath != "" { @@ -248,6 +256,7 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { disableAlerts: g.disableAlerts, extraLabels: g.extraLabels, sloPluginRepo: pluginsRepo, + extraSLOPlugins: cmdLevelSLOPlugins, } for _, genTarget := range genTargets { @@ -308,6 +317,7 @@ type generator struct { disableAlerts bool extraLabels map[string]string sloPluginRepo *storagefs.FilePluginRepo + extraSLOPlugins []model.PromSLOPluginMetadata } // GeneratePrometheus generates the SLOs based on a raw regular Prometheus spec format input and outs a Prometheus raw yaml. @@ -473,6 +483,7 @@ func (g generator) generateRules(ctx context.Context, info model.Info, slos mode alertRuleGen, }, SLOPluginGetter: g.sloPluginRepo, + ExtraPlugins: g.extraSLOPlugins, Logger: g.logger, }) if err != nil { diff --git a/cmd/sloth/commands/helpers.go b/cmd/sloth/commands/helpers.go index 72c05b19..574111a9 100644 --- a/cmd/sloth/commands/helpers.go +++ b/cmd/sloth/commands/helpers.go @@ -3,6 +3,7 @@ package commands import ( "bytes" "context" + "encoding/json" "fmt" "io/fs" "os" @@ -15,6 +16,7 @@ import ( pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" storagefs "github.com/slok/sloth/internal/storage/fs" + "github.com/slok/sloth/pkg/common/model" ) var ( @@ -99,3 +101,28 @@ func discoverSLOManifests(logger log.Logger, exclude, include *regexp.Regexp, pa return paths, nil } + +func mapCmdPluginToModel(ctx context.Context, jsonPlugins []string) ([]model.PromSLOPluginMetadata, error) { + type jsonPlugin struct { + ID string `json:"id"` + Config json.RawMessage `json:"config"` + Priority int `json:"priority"` + } + + plugins := []model.PromSLOPluginMetadata{} + for _, jp := range jsonPlugins { + p := &jsonPlugin{} + err := json.Unmarshal([]byte(jp), p) + if err != nil { + return nil, fmt.Errorf("could not load cmd plugin json config %q: %w", jp, err) + } + + plugins = append(plugins, model.PromSLOPluginMetadata{ + ID: p.ID, + Config: p.Config, + Priority: p.Priority, + }) + } + + return plugins, nil +} diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index a2cc4b61..4329710a 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -72,6 +72,7 @@ type kubeControllerCommand struct { pluginsPaths []string sloPeriodWindowsPath string sloPeriod string + sloPlugins []string } // NewKubeControllerCommand returns the Kubernetes controller command. @@ -98,6 +99,7 @@ func NewKubeControllerCommand(app *kingpin.Application) Command { cmd.Flag("plugins-path", "The path to SLI and SLO plugins (can be repeated).").Short('p').StringsVar(&c.pluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) + cmd.Flag("slo-plugins", `SLO plugins chain declaration in JSON format '{"id": "foo","priority": 0,"config": "{}"}' .`).Short('s').StringsVar(&c.sloPlugins) return c } @@ -119,6 +121,12 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error return err } + // Load SLO plugin declarations at CMD level. + cmdLevelSLOPlugins, err := mapCmdPluginToModel(ctx, k.sloPlugins) + if err != nil { + return fmt.Errorf("could not load slo plugin declarations: %w", err) + } + // Windows repository. var wfs fs.FS if k.sloPeriodWindowsPath != "" { @@ -346,6 +354,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error alertRuleGen, }, SLOPluginGetter: pluginsRepo, + ExtraPlugins: cmdLevelSLOPlugins, Logger: generatorLogger{Logger: logger}, }) if err != nil { diff --git a/internal/app/generate/generate.go b/internal/app/generate/generate.go index 41cbbbfa..8ae4487c 100644 --- a/internal/app/generate/generate.go +++ b/internal/app/generate/generate.go @@ -38,6 +38,7 @@ type SLOPluginGetter interface { type ServiceConfig struct { AlertGenerator AlertGenerator DefaultPlugins []SLOProcessor + ExtraPlugins []model.PromSLOPluginMetadata SLOPluginGetter SLOPluginGetter Logger log.Logger } @@ -115,6 +116,7 @@ type Service struct { alertGen AlertGenerator sloPluginGetter SLOPluginGetter defaultPlugins []SLOProcessor + extraPlugins []model.PromSLOPluginMetadata logger log.Logger } @@ -129,6 +131,7 @@ func NewService(config ServiceConfig) (*Service, error) { alertGen: config.AlertGenerator, sloPluginGetter: config.SLOPluginGetter, defaultPlugins: config.DefaultPlugins, + extraPlugins: config.ExtraPlugins, logger: config.Logger, }, nil } @@ -197,7 +200,11 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, sloGroup mode // That way we create the final processor list: pre-default + default + post-default. preDefault := []SLOProcessor{} postDefault := []SLOProcessor{} - sloPluginMetadata := append([]model.PromSLOPluginMetadata{}, slo.Plugins.Plugins...) + sloPluginMetadata := []model.PromSLOPluginMetadata{} + if !slo.Plugins.OverridePlugins { + sloPluginMetadata = append(sloPluginMetadata, s.extraPlugins...) // App level SLO plugins. + } + sloPluginMetadata = append(sloPluginMetadata, slo.Plugins.Plugins...) // SLO (group and/or individual) level plugins. slices.SortStableFunc(sloPluginMetadata, func(a, b model.PromSLOPluginMetadata) int { return cmp.Compare(a.Priority, b.Priority) }) @@ -225,8 +232,8 @@ func (s Service) generateSLO(ctx context.Context, info model.Info, sloGroup mode // Prepare processors. sloProcessors := preDefault - // Add default plugins if we don't want to override the default ones. - if !slo.Plugins.OverrideDefaultPlugins { + // Add default plugins if we don't want to override the plugins. + if !slo.Plugins.OverridePlugins { sloProcessors = append(sloProcessors, s.defaultPlugins...) } sloProcessors = append(sloProcessors, postDefault...) diff --git a/internal/app/generate/generate_test.go b/internal/app/generate/generate_test.go index 1f5964e1..08044da1 100644 --- a/internal/app/generate/generate_test.go +++ b/internal/app/generate/generate_test.go @@ -41,10 +41,11 @@ func (p testPluginAlertRuleAppender) ProcessSLO(ctx context.Context, request *pl func TestIntegrationAppServiceGenerate(t *testing.T) { tests := map[string]struct { - mocks func(mspg *generatemock.SLOPluginGetter) - req generate.Request - expResp generate.Response - expErr bool + mocks func(mspg *generatemock.SLOPluginGetter) + extraSLOPlugins []model.PromSLOPluginMetadata + req generate.Request + expResp generate.Response + expErr bool }{ "If no SLOs are requested it should error.": { mocks: func(mspg *generatemock.SLOPluginGetter) {}, @@ -436,6 +437,10 @@ or }, "Having multiple SLO plugins should execute the plugins in order and generate the rules correctly.": { + extraSLOPlugins: []model.PromSLOPluginMetadata{ + {ID: "test-plugin-99", Priority: 999}, + {ID: "test-plugin-98", Priority: 998}, + }, mocks: func(mspg *generatemock.SLOPluginGetter) { mspg.On("GetSLOPlugin", mock.Anything, "test-plugin1").Once().Return(&pluginengineslo.Plugin{ ID: "test-plugin1", @@ -480,6 +485,20 @@ or return testPluginAlertRuleAppender{rule: rulefmt.Rule{Expr: "test7"}}, nil }, }, nil) + + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin-98").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin2", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertRuleAppender{rule: rulefmt.Rule{Expr: "test98"}}, nil + }, + }, nil) + + mspg.On("GetSLOPlugin", mock.Anything, "test-plugin-99").Once().Return(&pluginengineslo.Plugin{ + ID: "test-plugin2", + PluginV1Factory: func(config json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return testPluginAlertRuleAppender{rule: rulefmt.Rule{Expr: "test99"}}, nil + }, + }, nil) }, req: generate.Request{ ExtraLabels: map[string]string{ @@ -797,6 +816,8 @@ or {Expr: "test6"}, {Expr: "test7"}, {Expr: "test1"}, + {Expr: "test98"}, + {Expr: "test99"}, {Expr: "test4"}, }}, }, @@ -805,7 +826,10 @@ or }, }, - "Having SLO plugins with default plugin override should execute only the configured plugins and ignore default plugin execution.": { + "Having SLO plugins with plugin override should execute only the configured plugins and ignore other levels declared plugin execution.": { + extraSLOPlugins: []model.PromSLOPluginMetadata{ + {ID: "test-plugin-00", Priority: -99}, + }, mocks: func(mspg *generatemock.SLOPluginGetter) { mspg.On("GetSLOPlugin", mock.Anything, "test-plugin1").Once().Return(&pluginengineslo.Plugin{ ID: "test-plugin1", @@ -841,7 +865,7 @@ or }, TicketAlertMeta: model.PromAlertMeta{Disable: true}, Plugins: model.SLOPlugins{ - OverrideDefaultPlugins: true, + OverridePlugins: true, Plugins: []model.PromSLOPluginMetadata{ {ID: "test-plugin1", Priority: 10}, }, @@ -872,7 +896,7 @@ or }, TicketAlertMeta: model.PromAlertMeta{Disable: true}, Plugins: model.SLOPlugins{ - OverrideDefaultPlugins: true, + OverridePlugins: true, Plugins: []model.PromSLOPluginMetadata{ {ID: "test-plugin1", Priority: 10}, }, @@ -903,6 +927,7 @@ or svc, err := generate.NewService(generate.ServiceConfig{ AlertGenerator: alert.NewGenerator(windowsRepo), SLOPluginGetter: mspg, + ExtraPlugins: test.extraSLOPlugins, }) require.NoError(err) diff --git a/internal/storage/io/k8s_sloth.go b/internal/storage/io/k8s_sloth.go index 3d64b3a3..012f73d8 100644 --- a/internal/storage/io/k8s_sloth.go +++ b/internal/storage/io/k8s_sloth.go @@ -133,8 +133,8 @@ func mapSpecToModel(ctx context.Context, defaultWindowPeriod time.Duration, plug PageAlertMeta: model.PromAlertMeta{Disable: true}, TicketAlertMeta: model.PromAlertMeta{Disable: true}, Plugins: model.SLOPlugins{ - OverrideDefaultPlugins: overridePlugins, - Plugins: plugins, + OverridePlugins: overridePlugins, + Plugins: plugins, }, } diff --git a/internal/storage/io/k8s_sloth_test.go b/internal/storage/io/k8s_sloth_test.go index dfb9e25a..28e6adbe 100644 --- a/internal/storage/io/k8s_sloth_test.go +++ b/internal/storage/io/k8s_sloth_test.go @@ -344,7 +344,7 @@ spec: }, }, Plugins: model.SLOPlugins{ - OverrideDefaultPlugins: true, + OverridePlugins: true, Plugins: []model.PromSLOPluginMetadata{ {ID: "test_plugin1", Priority: 100, Config: json.RawMessage([]byte(`{"k1":"v1","k3":true}`))}, }, diff --git a/internal/storage/io/sloth.go b/internal/storage/io/sloth.go index 96e94d8f..49a04647 100644 --- a/internal/storage/io/sloth.go +++ b/internal/storage/io/sloth.go @@ -111,8 +111,8 @@ func (l SlothPrometheusYAMLSpecLoader) mapSpecToModel(ctx context.Context, spec PageAlertMeta: model.PromAlertMeta{Disable: true}, TicketAlertMeta: model.PromAlertMeta{Disable: true}, Plugins: model.SLOPlugins{ - OverrideDefaultPlugins: overridePlugins, - Plugins: plugins, + OverridePlugins: overridePlugins, + Plugins: plugins, }, } diff --git a/internal/storage/io/sloth_test.go b/internal/storage/io/sloth_test.go index 41a1809d..e979b839 100644 --- a/internal/storage/io/sloth_test.go +++ b/internal/storage/io/sloth_test.go @@ -362,7 +362,7 @@ slos: }, }, Plugins: model.SLOPlugins{ - OverrideDefaultPlugins: true, + OverridePlugins: true, Plugins: []model.PromSLOPluginMetadata{ {ID: "test_plugin1", Priority: 100, Config: json.RawMessage([]byte(`{"k1":"v1","k3":true}`))}, }, diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index 7a63a5c9..fd17590f 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -49,8 +49,8 @@ type PromSLO struct { } type SLOPlugins struct { - OverrideDefaultPlugins bool - Plugins []PromSLOPluginMetadata + OverridePlugins bool // If true, the default, app and other declared plugins at other levels will be overridden by the ones declared in this struct. + Plugins []PromSLOPluginMetadata } type PromSLOPluginMetadata struct { diff --git a/pkg/common/validation/slo.go b/pkg/common/validation/slo.go index 1396789c..0e741d43 100644 --- a/pkg/common/validation/slo.go +++ b/pkg/common/validation/slo.go @@ -122,8 +122,8 @@ func isValidAlert(alert model.PromAlertMeta, dialect SLODialectValidator) error } func isValidPlugins(slo model.PromSLO) error { - if slo.Plugins.OverrideDefaultPlugins && len(slo.Plugins.Plugins) == 0 { - return fmt.Errorf("override default plugins is set but no plugins are defined") + if slo.Plugins.OverridePlugins && len(slo.Plugins.Plugins) == 0 { + return fmt.Errorf("override plugins is set but no plugins are defined") } for _, p := range slo.Plugins.Plugins { diff --git a/pkg/common/validation/slo_test.go b/pkg/common/validation/slo_test.go index 5037291e..88d1832e 100644 --- a/pkg/common/validation/slo_test.go +++ b/pkg/common/validation/slo_test.go @@ -497,14 +497,14 @@ func TestModelValidationSpecForPrometheusBackend(t *testing.T) { expErrMessage: `invalid plugins: plugin ID is required`, }, - "SLO plugins should be declared if override default plugins is used.": { + "SLO plugins should be declared if override plugins is used.": { slo: func() model.PromSLO { s := getGoodSLO() - s.Plugins.OverrideDefaultPlugins = true + s.Plugins.OverridePlugins = true s.Plugins.Plugins = []model.PromSLOPluginMetadata{} return s }, - expErrMessage: `invalid plugins: override default plugins is set but no plugins are defined`, + expErrMessage: `invalid plugins: override plugins is set but no plugins are defined`, }, } From cdd29c4b52ee647b8913b5dcacdaef4b03a64a4f Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sun, 4 May 2025 13:54:01 +0200 Subject: [PATCH 059/173] Add SLO plugin readmes Signed-off-by: Xabier Larrakoetxea --- .../plugin/slo/core/alert_rules_v1/README.md | 30 ++++++++++++++ internal/plugin/slo/core/debug_v1/README.md | 39 +++++++++++++++++++ .../slo/core/metadata_rules_v1/README.md | 31 +++++++++++++++ internal/plugin/slo/core/noop_v1/README.md | 24 ++++++++++++ .../plugin/slo/core/sli_rules_v1/README.md | 39 +++++++++++++++++++ .../plugin/slo/core/validate_v1/README.md | 30 ++++++++++++++ 6 files changed, 193 insertions(+) create mode 100644 internal/plugin/slo/core/alert_rules_v1/README.md create mode 100644 internal/plugin/slo/core/debug_v1/README.md create mode 100644 internal/plugin/slo/core/metadata_rules_v1/README.md create mode 100644 internal/plugin/slo/core/noop_v1/README.md create mode 100644 internal/plugin/slo/core/sli_rules_v1/README.md create mode 100644 internal/plugin/slo/core/validate_v1/README.md diff --git a/internal/plugin/slo/core/alert_rules_v1/README.md b/internal/plugin/slo/core/alert_rules_v1/README.md new file mode 100644 index 00000000..bd50832e --- /dev/null +++ b/internal/plugin/slo/core/alert_rules_v1/README.md @@ -0,0 +1,30 @@ +# sloth.dev/core/alert_rules/v1 + +This plugin generates multi-window, multi-burn-rate (MWMB) Prometheus alerting rules for SLOs based on pre-existing SLI recording rules. It is part of Sloth's default behavior and is responsible for producing both **page** and **ticket** severity alerts, depending on the SLO configuration. + +It supports advanced alerting patterns using short and long burn windows to detect fast and slow error budget consumption. + +## Config + +None + +## Env vars + +None + +## Order requirement + +This plugin should generally run after validation plugins. + +## Usage examples + +### Default usage (auto-loaded) + +This plugin is automatically executed by default when no custom plugin chain is defined. + +### Explicit inclusion + +```yaml +chain: + - id: "sloth.dev/core/alert_rules/v1" +``` diff --git a/internal/plugin/slo/core/debug_v1/README.md b/internal/plugin/slo/core/debug_v1/README.md new file mode 100644 index 00000000..67c100c6 --- /dev/null +++ b/internal/plugin/slo/core/debug_v1/README.md @@ -0,0 +1,39 @@ +# sloth.dev/core/debug/v1 + +A simple debug plugin used for testing and debugging purposes. For example it can be used to print the SLO mutations of the objects while developing other plugins in a plugin chain easily. + +The plugin will use `debug` level on the logger, so you will need to run sloth with in debug mode to check the debug messages from this plugin. + +## Config + +- `msg`(**Optional**): A custom message to be logged by the plugin. +- `result`(**Optional**): If `true` logs the plugin received result struct. +- `request`(**Optional**): If `true` logs the plugin received request struct. + +## Env vars + +None + +## Order requirement + +None + +## Usage examples + +### Simple message log + +```yaml +chain: + - id: "sloth.dev/core/debug/v1" + config: + msg: "Hello world" +``` + +### Log everything as last plugin + +```yaml +chain: + - id: "sloth.dev/core/debug/v1" + priority: 9999999 + config: {msg: "Last plugin", result: true, request: true} +``` diff --git a/internal/plugin/slo/core/metadata_rules_v1/README.md b/internal/plugin/slo/core/metadata_rules_v1/README.md new file mode 100644 index 00000000..d13d5681 --- /dev/null +++ b/internal/plugin/slo/core/metadata_rules_v1/README.md @@ -0,0 +1,31 @@ +# sloth.dev/core/metadata_rules/v1 + +This plugin generates a standard set of Prometheus recording rules that provide metadata about the SLO. These rules are used by Sloth by default and help to enrich the SLO with information such as burn rates, objective ratios, time period lengths, and general descriptive labels. + +It is automatically included by Sloth unless explicitly disabled. While it does not need custom configuration, understanding its output can be useful for integration with dashboards or alerting systems. + +## Config + +None + +## Env vars + +None + +## Order requirement + +This plugin should generally run after validation plugins. + +## Usage examples + +### Default usage (auto-loaded) + +This plugin is automatically executed by default when no custom plugin chain is defined. + +### Explicit inclusion + +```yaml + +chain: + - id: "sloth.dev/core/metadata_rules/v1" +``` diff --git a/internal/plugin/slo/core/noop_v1/README.md b/internal/plugin/slo/core/noop_v1/README.md new file mode 100644 index 00000000..49787a6f --- /dev/null +++ b/internal/plugin/slo/core/noop_v1/README.md @@ -0,0 +1,24 @@ +# sloth.dev/core/noop/v1 + +This plugin performs no operation and is intended purely as an example or placeholder. It can be used to test the plugin chain mechanism or serve as a minimal reference implementation for building new SLO plugins. + +## Config + +None + +## Env vars + +None + +## Order requirement + +None + +## Usage examples + +### No-op plugin in chain + +```yaml +chain: + - id: "sloth.dev/core/noop/v1" +``` diff --git a/internal/plugin/slo/core/sli_rules_v1/README.md b/internal/plugin/slo/core/sli_rules_v1/README.md new file mode 100644 index 00000000..47f03f57 --- /dev/null +++ b/internal/plugin/slo/core/sli_rules_v1/README.md @@ -0,0 +1,39 @@ +# sloth.dev/core/sli_rules/v1 + +This plugin generates the Prometheus **SLI error ratio recording rules** for each required time window in the SLO. These rules are used by other plugins (such as alerting and metadata) and are a foundational part of Sloth's default behavior. + +It supports both **event-based** and **raw query-based** SLIs, and it includes an optional optimization mode to reduce Prometheus resource usage by computing longer windows from short-window recording rules. This plugin is executed automatically by default in Sloth. + +## Config + +- `disableOptimized`(**Optional**, `bool`): If `true`, disables optimized rule generation for long SLI windows. Optimized rules use short-window recording rules to derive long-window SLIs with lower Prometheus resource usage, at the cost of reduced accuracy. Defaults to `false`. + +## Env vars + +None + +## Order requirement + +This plugin should generally run after validation plugins. + +## Usage examples + +### Default usage (auto-loaded) + +This plugin is automatically executed by default when no custom plugin chain is defined. + +### With optimizations + +```yaml +chain: + - id: "sloth.dev/core/sli_rules/v1" +``` + +### Disable optimization + +```yaml +chain: + - id: "sloth.dev/core/sli_rules/v1" + config: + disableOptimized: true +``` diff --git a/internal/plugin/slo/core/validate_v1/README.md b/internal/plugin/slo/core/validate_v1/README.md new file mode 100644 index 00000000..4e2a1c3b --- /dev/null +++ b/internal/plugin/slo/core/validate_v1/README.md @@ -0,0 +1,30 @@ +# sloth.dev/core/validate/v1 + +This plugin validates the SLO specification to ensure it is correct and well-formed according to the **Prometheus SLO dialect**. It is the **first plugin executed** by Sloth and acts as a safety check before any rules are generated or other plugins are run. + +This plugin is **enabled by default** and should only be disabled if you're using a custom backend (e.g., VictoriaMetrics, Loki) that requires a different validation logic. In that case, you should replace this plugin with your own validator plugin tailored to the target system. + +## Config + +None + +## Env vars + +None + +## Order requirement + +This plugin must be placed **first** in the plugin chain to validate the SLO before any further processing is done. + +## Usage examples + +### Default usage (auto-loaded) + +This plugin is automatically executed as the first step in the default plugin chain. + +### Explicit usage + +```yaml +chain: + - id: "sloth.dev/core/validate/v1" +```` From 78077ba218e85a21070f9bf55b4af4b081083548 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Mon, 5 May 2025 18:57:49 +0200 Subject: [PATCH 060/173] Fix readme on validate plugin Signed-off-by: Xabier Larrakoetxea --- internal/plugin/slo/core/validate_v1/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/plugin/slo/core/validate_v1/README.md b/internal/plugin/slo/core/validate_v1/README.md index 4e2a1c3b..07442a1c 100644 --- a/internal/plugin/slo/core/validate_v1/README.md +++ b/internal/plugin/slo/core/validate_v1/README.md @@ -27,4 +27,4 @@ This plugin is automatically executed as the first step in the default plugin ch ```yaml chain: - id: "sloth.dev/core/validate/v1" -```` +``` From e781ac245cf700967d7e4b164b2e657a925292b4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 May 2025 07:55:55 +0000 Subject: [PATCH 061/173] build(deps): bump golang in /docker/prod Bumps golang from 1.24.2-alpine to 1.24.3-alpine. --- updated-dependencies: - dependency-name: golang dependency-version: 1.24.3-alpine dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docker/prod/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 4e4b1c2a..4ffea32b 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,7 +1,7 @@ # Set also `ARCH` ARG here so we can use it on all the `FROM`s. ARG ARCH -FROM golang:1.24.2-alpine as build-stage +FROM golang:1.24.3-alpine as build-stage LABEL org.opencontainers.image.source=https://github.com/slok/sloth From 830a4432c58a4ddf7978b91d13466c38d3349d1e Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 8 May 2025 10:16:05 +0200 Subject: [PATCH 062/173] add nodeSelector property in deployment --- deploy/kubernetes/helm/sloth/templates/deployment.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deploy/kubernetes/helm/sloth/templates/deployment.yaml b/deploy/kubernetes/helm/sloth/templates/deployment.yaml index f856e092..e9da4d88 100644 --- a/deploy/kubernetes/helm/sloth/templates/deployment.yaml +++ b/deploy/kubernetes/helm/sloth/templates/deployment.yaml @@ -29,6 +29,10 @@ spec: {{- end }} {{- with .Values.tolerations }} tolerations: +{{ toYaml . | trim | indent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: {{ toYaml . | trim | indent 8 }} {{- end }} containers: From a0b29fdfd725b35e4f7eb419892cd98c55093286 Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 8 May 2025 10:16:23 +0200 Subject: [PATCH 063/173] add values nodeSelector example --- deploy/kubernetes/helm/sloth/values.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deploy/kubernetes/helm/sloth/values.yaml b/deploy/kubernetes/helm/sloth/values.yaml index 194a925a..9ed6f836 100644 --- a/deploy/kubernetes/helm/sloth/values.yaml +++ b/deploy/kubernetes/helm/sloth/values.yaml @@ -71,6 +71,10 @@ customSloConfig: # value: spot # effect: NoSchedule +# add deployment pod nodeSelector +# nodeSelector: +# kubernetes.io/arch: "arm64" + securityContext: pod: null # fsGroup: 100 From a583d8c06dc6eb27702785c9e4d062fadacf4d6b Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 8 May 2025 10:28:06 +0200 Subject: [PATCH 064/173] add testing to new nodeSelector property in deployment tests --- .../helm/sloth/tests/testdata/output/deployment_custom.yaml | 3 +++ .../tests/testdata/output/deployment_custom_no_extras.yaml | 3 +++ .../tests/testdata/output/deployment_custom_slo_config.yaml | 3 +++ deploy/kubernetes/helm/sloth/tests/values_test.go | 5 +++++ 4 files changed, 14 insertions(+) diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml index ad3832ca..1331a9d2 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml @@ -37,6 +37,9 @@ spec: runAsGroup: 1000 runAsNonRoot: true runAsUser: 100 + nodeSelector: + k1: v1 + k2: v2 containers: - name: sloth image: slok/sloth-test:v1.42.42 diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_no_extras.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_no_extras.yaml index c2d4ab0d..c67e2204 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_no_extras.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_no_extras.yaml @@ -37,6 +37,9 @@ spec: runAsGroup: 1000 runAsNonRoot: true runAsUser: 100 + nodeSelector: + k1: v1 + k2: v2 containers: - name: sloth image: slok/sloth-test:v1.42.42 diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_slo_config.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_slo_config.yaml index 6dbd8b11..10fb51fc 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_slo_config.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_slo_config.yaml @@ -38,6 +38,9 @@ spec: runAsGroup: 1000 runAsNonRoot: true runAsUser: 100 + nodeSelector: + k1: v1 + k2: v2 containers: - name: sloth image: slok/sloth-test:v1.42.42 diff --git a/deploy/kubernetes/helm/sloth/tests/values_test.go b/deploy/kubernetes/helm/sloth/tests/values_test.go index ec54feae..9de85763 100644 --- a/deploy/kubernetes/helm/sloth/tests/values_test.go +++ b/deploy/kubernetes/helm/sloth/tests/values_test.go @@ -34,6 +34,11 @@ func customValues() msi { }, }, + "nodeSelector": msi{ + "k1": "v1", + "k2": "v2", + }, + "commonPlugins": msi{ "enabled": true, "gitRepo": msi{ From 586d8d1dfa757bfe1441836255c8860f037c5b39 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 10 May 2025 10:38:48 +0200 Subject: [PATCH 065/173] Allow disabling default plugins using a cmd flag on CLI and k8s controller modes Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 2 + cmd/sloth/commands/generate.go | 117 +++++++++------------------- cmd/sloth/commands/helpers.go | 61 +++++++++++++++ cmd/sloth/commands/k8scontroller.go | 91 +++++++--------------- 4 files changed, 128 insertions(+), 143 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0378433a..8962f5fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ - SLO plugins can access env vars and use OS/exec by default. - Allow `github.com/caarlos0/env/v11` module in SLO plugins. - Add `--slo-plugins` and `-s` flag (`generate` and `k8s controller`) to be able to declare SLO plugins at cmd level, these plugins will be applied to all SLOs. +- Add `--disable-default-slo-plugins` flag (`generate` and `k8s controller`) to be able to disable default Sloth SLO plugins. +- Helm chart supports node selector. ## [v0.12.0] - 2025-03-27 diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index a0d7040b..9a803167 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -20,10 +20,6 @@ import ( "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/log" - plugincorealertrulesv1 "github.com/slok/sloth/internal/plugin/slo/core/alert_rules_v1" - plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" - plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" - plugincorevalidatev1 "github.com/slok/sloth/internal/plugin/slo/core/validate_v1" "github.com/slok/sloth/internal/storage" storagefs "github.com/slok/sloth/internal/storage/fs" storageio "github.com/slok/sloth/internal/storage/io" @@ -33,17 +29,18 @@ import ( ) type generateCommand struct { - slosInput string - slosOut string - slosExcludeRegex string - slosIncludeRegex string - disableRecordings bool - disableAlerts bool - extraLabels map[string]string - pluginsPaths []string - sloPeriodWindowsPath string - sloPeriod string - sloPlugins []string + slosInput string + slosOut string + slosExcludeRegex string + slosIncludeRegex string + disableRecordings bool + disableAlerts bool + extraLabels map[string]string + pluginsPaths []string + sloPeriodWindowsPath string + sloPeriod string + sloPlugins []string + disableDefaultSLOPlugins bool } // NewGenerateCommand returns the generate command. @@ -62,6 +59,7 @@ func NewGenerateCommand(app *kingpin.Application) Command { cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) cmd.Flag("slo-plugins", `SLO plugins chain declaration in JSON format '{"id": "foo","priority": 0,"config": "{}"}' (Can be repeated).`).Short('s').StringsVar(&c.sloPlugins) + cmd.Flag("disable-default-slo-plugins", `Disables the default SLO plugins, normally used along with custom SLO plugins to fully customize Sloth behavior`).BoolVar(&c.disableDefaultSLOPlugins) return c } @@ -250,13 +248,14 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { } gen := generator{ - logger: logger, - windowsRepo: windowsRepo, - disableRecordings: g.disableRecordings, - disableAlerts: g.disableAlerts, - extraLabels: g.extraLabels, - sloPluginRepo: pluginsRepo, - extraSLOPlugins: cmdLevelSLOPlugins, + logger: logger, + windowsRepo: windowsRepo, + disableRecordings: g.disableRecordings, + disableAlerts: g.disableAlerts, + extraLabels: g.extraLabels, + sloPluginRepo: pluginsRepo, + extraSLOPlugins: cmdLevelSLOPlugins, + disableDefaultSLOPlugins: g.disableDefaultSLOPlugins, } for _, genTarget := range genTargets { @@ -311,13 +310,14 @@ type generateTarget struct { } type generator struct { - logger log.Logger - windowsRepo alert.WindowsRepo - disableRecordings bool - disableAlerts bool - extraLabels map[string]string - sloPluginRepo *storagefs.FilePluginRepo - extraSLOPlugins []model.PromSLOPluginMetadata + logger log.Logger + windowsRepo alert.WindowsRepo + disableRecordings bool + disableAlerts bool + extraLabels map[string]string + sloPluginRepo *storagefs.FilePluginRepo + extraSLOPlugins []model.PromSLOPluginMetadata + disableDefaultSLOPlugins bool } // GeneratePrometheus generates the SLOs based on a raw regular Prometheus spec format input and outs a Prometheus raw yaml. @@ -425,63 +425,20 @@ func (g generator) GenerateOpenSLO(ctx context.Context, slos model.PromSLOGroup, // generate is the main generator logic that all the spec types and storers share. Mainly has the logic of the generate app service. func (g generator) generateRules(ctx context.Context, info model.Info, slos model.PromSLOGroup) (*generate.Response, error) { - // Disable recording rules if required. - var sliRuleGen generate.SLOProcessor = generate.NoopPlugin - var metaRuleGen generate.SLOProcessor = generate.NoopPlugin - if !g.disableRecordings { - sliPlugin, err := generate.NewSLOProcessorFromSLOPluginV1( - plugincoreslirulesv1.NewPlugin, - g.logger.WithValues(log.Kv{"plugin": plugincoreslirulesv1.PluginID}), - plugincoreslirulesv1.PluginConfig{}, - ) + var defSLOPlugins = []generate.SLOProcessor{} + var err error + if !g.disableDefaultSLOPlugins { + // Load default slo plugins. + defSLOPlugins, err = createDefaultSLOPlugins(g.logger, g.disableRecordings, g.disableAlerts) if err != nil { - return nil, fmt.Errorf("could not create SLI rules plugin: %w", err) + return nil, fmt.Errorf("could not create default slo plugins: %w", err) } - sliRuleGen = sliPlugin - - metadataPlugin, err := generate.NewSLOProcessorFromSLOPluginV1( - plugincoremetadatarulesv1.NewPlugin, - g.logger.WithValues(log.Kv{"plugin": plugincoremetadatarulesv1.PluginID}), - nil, - ) - if err != nil { - return nil, fmt.Errorf("could not create metadata rules plugin: %w", err) - } - metaRuleGen = metadataPlugin - } - - validatePlugin, err := generate.NewSLOProcessorFromSLOPluginV1( - plugincorevalidatev1.NewPlugin, - g.logger.WithValues(log.Kv{"plugin": plugincorevalidatev1.PluginID}), - nil, - ) - if err != nil { - return nil, fmt.Errorf("could not create SLO validate plugin: %w", err) - } - - // Disable alert rules if required. - var alertRuleGen generate.SLOProcessor = generate.NoopPlugin - if !g.disableAlerts { - plugin, err := generate.NewSLOProcessorFromSLOPluginV1( - plugincorealertrulesv1.NewPlugin, - g.logger.WithValues(log.Kv{"plugin": plugincorealertrulesv1.PluginID}), - nil, - ) - if err != nil { - return nil, fmt.Errorf("could not create alert rules plugin: %w", err) - } - alertRuleGen = plugin } // Generate. controller, err := generate.NewService(generate.ServiceConfig{ - AlertGenerator: alert.NewGenerator(g.windowsRepo), - DefaultPlugins: []generate.SLOProcessor{ - validatePlugin, - sliRuleGen, - metaRuleGen, - alertRuleGen, - }, + AlertGenerator: alert.NewGenerator(g.windowsRepo), + DefaultPlugins: defSLOPlugins, SLOPluginGetter: g.sloPluginRepo, ExtraPlugins: g.extraSLOPlugins, Logger: g.logger, diff --git a/cmd/sloth/commands/helpers.go b/cmd/sloth/commands/helpers.go index 574111a9..37f38318 100644 --- a/cmd/sloth/commands/helpers.go +++ b/cmd/sloth/commands/helpers.go @@ -11,8 +11,13 @@ import ( "regexp" "strings" + "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/plugin" + plugincorealertrulesv1 "github.com/slok/sloth/internal/plugin/slo/core/alert_rules_v1" + plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" + plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" + plugincorevalidatev1 "github.com/slok/sloth/internal/plugin/slo/core/validate_v1" pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" storagefs "github.com/slok/sloth/internal/storage/fs" @@ -126,3 +131,59 @@ func mapCmdPluginToModel(ctx context.Context, jsonPlugins []string) ([]model.Pro return plugins, nil } + +func createDefaultSLOPlugins(logger log.Logger, disableRecordings, disableAlerts bool) ([]generate.SLOProcessor, error) { + var sliRuleGen generate.SLOProcessor = generate.NoopPlugin + var metaRuleGen generate.SLOProcessor = generate.NoopPlugin + if !disableRecordings { + sliPlugin, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincoreslirulesv1.NewPlugin, + logger.WithValues(log.Kv{"plugin": plugincoreslirulesv1.PluginID}), + plugincoreslirulesv1.PluginConfig{}, + ) + if err != nil { + return nil, fmt.Errorf("could not create SLI rules plugin: %w", err) + } + sliRuleGen = sliPlugin + + metadataPlugin, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincoremetadatarulesv1.NewPlugin, + logger.WithValues(log.Kv{"plugin": plugincoremetadatarulesv1.PluginID}), + nil, + ) + if err != nil { + return nil, fmt.Errorf("could not create metadata rules plugin: %w", err) + } + metaRuleGen = metadataPlugin + } + + validatePlugin, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincorevalidatev1.NewPlugin, + logger.WithValues(log.Kv{"plugin": plugincorevalidatev1.PluginID}), + nil, + ) + if err != nil { + return nil, fmt.Errorf("could not create SLO validate plugin: %w", err) + } + + // Disable alert rules if required. + var alertRuleGen generate.SLOProcessor = generate.NoopPlugin + if !disableAlerts { + plugin, err := generate.NewSLOProcessorFromSLOPluginV1( + plugincorealertrulesv1.NewPlugin, + logger.WithValues(log.Kv{"plugin": plugincorealertrulesv1.PluginID}), + nil, + ) + if err != nil { + return nil, fmt.Errorf("could not create alert rules plugin: %w", err) + } + alertRuleGen = plugin + } + + return []generate.SLOProcessor{ + validatePlugin, + sliRuleGen, + metaRuleGen, + alertRuleGen, + }, nil +} diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index 4329710a..3ec222c3 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -33,10 +33,6 @@ import ( "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/app/kubecontroller" "github.com/slok/sloth/internal/log" - plugincorealertrulesv1 "github.com/slok/sloth/internal/plugin/slo/core/alert_rules_v1" - plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" - plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" - plugincorevalidatev1 "github.com/slok/sloth/internal/plugin/slo/core/validate_v1" "github.com/slok/sloth/internal/storage" storageio "github.com/slok/sloth/internal/storage/io" storagek8s "github.com/slok/sloth/internal/storage/k8s" @@ -56,23 +52,24 @@ const ( ) type kubeControllerCommand struct { - extraLabels map[string]string - workers int - kubeConfig string - kubeContext string - resyncInterval time.Duration - namespace string - labelSelector string - kubeLocal bool - runMode string - metricsPath string - hotReloadPath string - hotReloadAddr string - metricsListenAddr string - pluginsPaths []string - sloPeriodWindowsPath string - sloPeriod string - sloPlugins []string + extraLabels map[string]string + workers int + kubeConfig string + kubeContext string + resyncInterval time.Duration + namespace string + labelSelector string + kubeLocal bool + runMode string + metricsPath string + hotReloadPath string + hotReloadAddr string + metricsListenAddr string + pluginsPaths []string + sloPeriodWindowsPath string + sloPeriod string + sloPlugins []string + disableDefaultSLOPlugins bool } // NewKubeControllerCommand returns the Kubernetes controller command. @@ -99,7 +96,8 @@ func NewKubeControllerCommand(app *kingpin.Application) Command { cmd.Flag("plugins-path", "The path to SLI and SLO plugins (can be repeated).").Short('p').StringsVar(&c.pluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) - cmd.Flag("slo-plugins", `SLO plugins chain declaration in JSON format '{"id": "foo","priority": 0,"config": "{}"}' .`).Short('s').StringsVar(&c.sloPlugins) + cmd.Flag("slo-plugins", `SLO plugins chain declaration in JSON format '{"id": "foo","priority": 0,"config": "{}"}' (Can be repeated).`).Short('s').StringsVar(&c.sloPlugins) + cmd.Flag("disable-default-slo-plugins", `Disables the default SLO plugins, normally used along with custom SLO plugins to fully customize Sloth behavior`).BoolVar(&c.disableDefaultSLOPlugins) return c } @@ -308,51 +306,18 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error ctx, cancel := context.WithCancel(ctx) defer cancel() - sliRuleGen, err := generate.NewSLOProcessorFromSLOPluginV1( - plugincoreslirulesv1.NewPlugin, - logger.WithValues(log.Kv{"plugin": plugincoreslirulesv1.PluginID}), - plugincoreslirulesv1.PluginConfig{}, - ) - if err != nil { - return fmt.Errorf("could not create SLI rules plugin: %w", err) - } - - metaRuleGen, err := generate.NewSLOProcessorFromSLOPluginV1( - plugincoremetadatarulesv1.NewPlugin, - logger.WithValues(log.Kv{"plugin": plugincoremetadatarulesv1.PluginID}), - nil, - ) - if err != nil { - return fmt.Errorf("could not create metadata rules plugin: %w", err) - } - - alertRuleGen, err := generate.NewSLOProcessorFromSLOPluginV1( - plugincorealertrulesv1.NewPlugin, - logger.WithValues(log.Kv{"plugin": plugincorealertrulesv1.PluginID}), - nil, - ) - if err != nil { - return fmt.Errorf("could not create alert rules plugin: %w", err) - } - - validatePlugin, err := generate.NewSLOProcessorFromSLOPluginV1( - plugincorevalidatev1.NewPlugin, - logger.WithValues(log.Kv{"plugin": plugincorevalidatev1.PluginID}), - nil, - ) - if err != nil { - return fmt.Errorf("could not create SLO validate plugin: %w", err) + defSLOPlugins := []generate.SLOProcessor{} + if !k.disableDefaultSLOPlugins { + defSLOPlugins, err = createDefaultSLOPlugins(logger, false, false) + if err != nil { + return fmt.Errorf("could not create default SLO plugins: %w", err) + } } // Create the generate app service (the one that the CLIs use). generator, err := generate.NewService(generate.ServiceConfig{ - AlertGenerator: alert.NewGenerator(windowsRepo), - DefaultPlugins: []generate.SLOProcessor{ - validatePlugin, - sliRuleGen, - metaRuleGen, - alertRuleGen, - }, + AlertGenerator: alert.NewGenerator(windowsRepo), + DefaultPlugins: defSLOPlugins, SLOPluginGetter: pluginsRepo, ExtraPlugins: cmdLevelSLOPlugins, Logger: generatorLogger{Logger: logger}, From b9bddf9830efc40de7e4cff856622dbde9f6853e Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 17 May 2025 09:58:28 +0200 Subject: [PATCH 066/173] Update Prometheus dep to v3.4.0 Signed-off-by: Xabier Larrakoetxea --- go.mod | 6 +- go.sum | 60 +++++++++---------- ...com-prometheus-prometheus-promql-parser.go | 2 + 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index 2254a2dd..65917654 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/common v0.63.0 - github.com/prometheus/prometheus v0.302.2-0.20250320225832-3d603d19575e + github.com/prometheus/prometheus v0.304.0 github.com/sirupsen/logrus v1.9.3 github.com/slok/reload v0.2.0 github.com/spotahome/kooper/v2 v2.8.0 @@ -41,7 +41,6 @@ require ( github.com/go-openapi/swag v0.23.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v1.0.0 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -55,7 +54,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.16.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect @@ -74,7 +73,6 @@ require ( golang.org/x/term v0.31.0 // indirect golang.org/x/text v0.24.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.31.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index fa1e75f2..ab3c6ac6 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,25 @@ -cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= -cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= -cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= -cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU= +cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/OpenSLO/oslo v0.12.0 h1:0zdxMgFE59TUKpe/L4+5ujgmJZW/kAuCOJbyqrTX4lc= github.com/OpenSLO/oslo v0.12.0/go.mod h1:6jyOTkqBCdkgqLXJ6WtJj7+o0rSexl6YZbWwnxpGwtU= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= -github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= -github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= +github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -59,8 +59,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= @@ -79,8 +79,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.5 h1:VgzTY2jogw3xt39CusEnFJWm7rlsq5yL5q9XdLOuP5g= -github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= @@ -136,14 +136,14 @@ github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0 h1:g/wIdMr github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0/go.mod h1:yEp9v3FEYT+iR2ujaXFVS8ugTIQk0Mx+wwXGXoF5az8= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= -github.com/prometheus/prometheus v0.302.2-0.20250320225832-3d603d19575e h1:04tg0Q3/UCwa5ATaa+a1s8aJasOuoPfeyL7FzNDc4rM= -github.com/prometheus/prometheus v0.302.2-0.20250320225832-3d603d19575e/go.mod h1:6Wxk+7sG0er66SyZFfcNTAflpes8MrRuBoZni+abkrI= +github.com/prometheus/prometheus v0.304.0 h1:otXBqfF7bbTcW7IrXrB6HMjo4dThQbayCPFr2yTlqrQ= +github.com/prometheus/prometheus v0.304.0/go.mod h1:ioGx2SGKTY+fLnJSQCdTHqARVldGNS8OlIe3kvp98so= github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8= github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAoaohfjqMU= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -231,19 +231,19 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm 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.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= -golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= +golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 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= -google.golang.org/api v0.224.0 h1:Ir4UPtDsNiwIOHdExr3fAj4xZ42QjK7uQte3lORLJwU= -google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= +google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM= +google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ= google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go b/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go index be0f437c..56acf3d8 100644 --- a/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go +++ b/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go @@ -48,6 +48,7 @@ func init() { "ERROR": reflect.ValueOf(constant.MakeFromLiteral("57353", token.INT, 0)), "EnableExperimentalFunctions": reflect.ValueOf(&parser.EnableExperimentalFunctions).Elem(), "EnrichParseError": reflect.ValueOf(parser.EnrichParseError), + "ExperimentalDurationExpr": reflect.ValueOf(&parser.ExperimentalDurationExpr).Elem(), "ExtractSelectors": reflect.ValueOf(parser.ExtractSelectors), "Functions": reflect.ValueOf(&parser.Functions).Elem(), "GAUGE_TYPE": reflect.ValueOf(constant.MakeFromLiteral("57437", token.INT, 0)), @@ -133,6 +134,7 @@ func init() { "AggregateExpr": reflect.ValueOf((*parser.AggregateExpr)(nil)), "BinaryExpr": reflect.ValueOf((*parser.BinaryExpr)(nil)), "Call": reflect.ValueOf((*parser.Call)(nil)), + "DurationExpr": reflect.ValueOf((*parser.DurationExpr)(nil)), "EvalStmt": reflect.ValueOf((*parser.EvalStmt)(nil)), "Expr": reflect.ValueOf((*parser.Expr)(nil)), "Expressions": reflect.ValueOf((*parser.Expressions)(nil)), From 3c1f5e5b4043cf2df866b792dcd68eb3662898de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 17 May 2025 08:05:00 +0000 Subject: [PATCH 067/173] build(deps): bump github.com/prometheus/common from 0.63.0 to 0.64.0 Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.63.0 to 0.64.0. - [Release notes](https://github.com/prometheus/common/releases) - [Changelog](https://github.com/prometheus/common/blob/main/RELEASE.md) - [Commits](https://github.com/prometheus/common/compare/v0.63.0...v0.64.0) --- updated-dependencies: - dependency-name: github.com/prometheus/common dependency-version: 0.64.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 14 +++++++------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index 65917654..6502bac2 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.0 github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0 github.com/prometheus/client_golang v1.22.0 - github.com/prometheus/common v0.63.0 + github.com/prometheus/common v0.64.0 github.com/prometheus/prometheus v0.304.0 github.com/sirupsen/logrus v1.9.3 github.com/slok/reload v0.2.0 @@ -66,12 +66,12 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/oauth2 v0.29.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/term v0.31.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/time v0.11.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/go.sum b/go.sum index ab3c6ac6..6b2be851 100644 --- a/go.sum +++ b/go.sum @@ -138,8 +138,8 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= -github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= github.com/prometheus/prometheus v0.304.0 h1:otXBqfF7bbTcW7IrXrB6HMjo4dThQbayCPFr2yTlqrQ= @@ -194,8 +194,8 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -204,27 +204,27 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn 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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 76c4d1776df3a49dc5ebdade898039b1f681504c Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Mon, 19 May 2025 08:47:04 +0200 Subject: [PATCH 068/173] Update to Kubernetes v1.33 Signed-off-by: Xabier Larrakoetxea --- .github/workflows/ci.yaml | 4 +-- CHANGELOG.md | 1 + README.md | 2 +- go.mod | 14 ++++------ go.sum | 28 ++++++++----------- .../versioned/fake/clientset_generated.go | 13 +++++++-- .../versioned/typed/sloth/v1/sloth_client.go | 12 ++------ .../sloth/v1/prometheusservicelevel.go | 16 +++++++++-- scripts/kubegen.sh | 2 +- 9 files changed, 51 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3cae4f7d..a8aed4ab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -72,7 +72,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - kubernetes: [1.29.14, 1.30.10, 1.31.6, 1.32.2] + kubernetes: [1.30.13, 1.31.9, 1.32.5, 1.33.1] steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 @@ -80,7 +80,7 @@ jobs: go-version-file: go.mod - name: Execute tests env: - KIND_VERSION: v0.27.0 + KIND_VERSION: v0.28.0 run: | # Get dependencies. echo "Getting dependencies..." diff --git a/CHANGELOG.md b/CHANGELOG.md index 8962f5fc..ec59b12a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - (BREAKING) `--sli-plugins-path`, `--slo-plugins-path`, `-m` args and it's env vars `SLOTH_SLI_PLUGINS_PATH`and `SLOTH_SLO_PLUGINS_PATH` have been removed in favor or `--plugins-path`, `-p` and it's env var `SLOTH_PLUGINS_PATH` that discovers and loads SLI and SLO plugins with a single flag. - Simplify validation and improve validation message by using custom logic instead of `go-playground/validator`. - (BREAKING) `--disable-optimized-rules` flag and associated env var has been removed. +- Update to Kubernetes v1.33. ### Added diff --git a/README.md b/README.md index 688ab620..850d7d35 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/slok/sloth)](https://goreportcard.com/report/github.com/slok/sloth) [![Apache 2 licensed](https://img.shields.io/badge/license-Apache2-blue.svg)](https://raw.githubusercontent.com/slok/sloth/master/LICENSE) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/slok/sloth)](https://github.com/slok/sloth/releases/latest) -![Kubernetes release](https://img.shields.io/badge/Kubernetes-v1.32-green?logo=Kubernetes&style=flat&color=326CE5&logoColor=white) +![Kubernetes release](https://img.shields.io/badge/Kubernetes-v1.33-green?logo=Kubernetes&style=flat&color=326CE5&logoColor=white) [![OpenSLO](https://img.shields.io/badge/OpenSLO-v1alpha-green?color=4974EA&style=flat)](https://github.com/OpenSLO/OpenSLO#slo) ## Project status diff --git a/go.mod b/go.mod index 6502bac2..d40d6cb3 100644 --- a/go.mod +++ b/go.mod @@ -14,13 +14,13 @@ require ( github.com/prometheus/prometheus v0.304.0 github.com/sirupsen/logrus v1.9.3 github.com/slok/reload v0.2.0 - github.com/spotahome/kooper/v2 v2.8.0 + github.com/spotahome/kooper/v2 v2.9.0 github.com/stretchr/testify v1.10.0 github.com/traefik/yaegi v0.16.1 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.32.3 - k8s.io/apimachinery v0.32.3 - k8s.io/client-go v0.32.3 + k8s.io/api v0.33.1 + k8s.io/apimachinery v0.33.1 + k8s.io/client-go v0.33.1 sigs.k8s.io/structured-merge-diff/v4 v4.7.0 ) @@ -40,10 +40,8 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/josharian/intern v1.0.0 // indirect @@ -55,7 +53,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/procfs v0.16.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -80,7 +78,7 @@ require ( k8s.io/apiextensions-apiserver v0.32.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect + k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect sigs.k8s.io/controller-runtime v0.20.4 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/go.sum b/go.sum index 6b2be851..2db2a458 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= @@ -71,8 +69,6 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -140,8 +136,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= -github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/prometheus v0.304.0 h1:otXBqfF7bbTcW7IrXrB6HMjo4dThQbayCPFr2yTlqrQ= github.com/prometheus/prometheus v0.304.0/go.mod h1:ioGx2SGKTY+fLnJSQCdTHqARVldGNS8OlIe3kvp98so= github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8= @@ -154,8 +150,8 @@ github.com/slok/reload v0.2.0 h1:8ezO7EsaYMUCX2g1ZAdHTxD0Mo9oDn2legZZBoFMUEI= github.com/slok/reload v0.2.0/go.mod h1:vyJiGsSZTUx08vDoEiVszBp8Jr+bsMwIK/U1pK7XTmI= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spotahome/kooper/v2 v2.8.0 h1:qAbT1kF5vC39jxr74dNqiBT9ZxxijBNqOsuZoJmgE88= -github.com/spotahome/kooper/v2 v2.8.0/go.mod h1:lBcjQOn/pbRS4F8jXZ5IRH7pcE3vhdXwz7cD9HtHiRM= +github.com/spotahome/kooper/v2 v2.9.0 h1:Iwk2eAZbp0M0Z4OZYkt12c7ENhyYe8byB0mtDvVYjq4= +github.com/spotahome/kooper/v2 v2.9.0/go.mod h1:im2PUUOGti/fXq0tUZaowG6cWJvgzS9BlX4ipz34c/E= 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= @@ -258,20 +254,20 @@ 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.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= -k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= -k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= -k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= -k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= -k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= +k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= diff --git a/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go b/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go index d4f76a5a..875640d0 100644 --- a/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go +++ b/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go @@ -7,6 +7,7 @@ import ( clientset "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" slothv1 "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1" fakeslothv1 "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/fake" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" @@ -34,9 +35,13 @@ func NewSimpleClientset(objects ...runtime.Object) *Clientset { cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + var opts metav1.ListOptions + if watchActcion, ok := action.(testing.WatchActionImpl); ok { + opts = watchActcion.ListOptions + } gvr := action.GetResource() ns := action.GetNamespace() - watch, err := o.Watch(gvr, ns) + watch, err := o.Watch(gvr, ns, opts) if err != nil { return false, nil, err } @@ -83,9 +88,13 @@ func NewClientset(objects ...runtime.Object) *Clientset { cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + var opts metav1.ListOptions + if watchActcion, ok := action.(testing.WatchActionImpl); ok { + opts = watchActcion.ListOptions + } gvr := action.GetResource() ns := action.GetNamespace() - watch, err := o.Watch(gvr, ns) + watch, err := o.Watch(gvr, ns, opts) if err != nil { return false, nil, err } diff --git a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/sloth_client.go b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/sloth_client.go index f9eafea4..ea01c0fd 100644 --- a/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/sloth_client.go +++ b/pkg/kubernetes/gen/clientset/versioned/typed/sloth/v1/sloth_client.go @@ -29,9 +29,7 @@ func (c *SlothV1Client) PrometheusServiceLevels(namespace string) PrometheusServ // where httpClient was generated with rest.HTTPClientFor(c). func NewForConfig(c *rest.Config) (*SlothV1Client, error) { config := *c - if err := setConfigDefaults(&config); err != nil { - return nil, err - } + setConfigDefaults(&config) httpClient, err := rest.HTTPClientFor(&config) if err != nil { return nil, err @@ -43,9 +41,7 @@ func NewForConfig(c *rest.Config) (*SlothV1Client, error) { // Note the http client provided takes precedence over the configured transport values. func NewForConfigAndClient(c *rest.Config, h *http.Client) (*SlothV1Client, error) { config := *c - if err := setConfigDefaults(&config); err != nil { - return nil, err - } + setConfigDefaults(&config) client, err := rest.RESTClientForConfigAndClient(&config, h) if err != nil { return nil, err @@ -68,7 +64,7 @@ func New(c rest.Interface) *SlothV1Client { return &SlothV1Client{c} } -func setConfigDefaults(config *rest.Config) error { +func setConfigDefaults(config *rest.Config) { gv := slothv1.SchemeGroupVersion config.GroupVersion = &gv config.APIPath = "/apis" @@ -77,8 +73,6 @@ func setConfigDefaults(config *rest.Config) error { if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() } - - return nil } // RESTClient returns a RESTClient that is used to communicate diff --git a/pkg/kubernetes/gen/informers/externalversions/sloth/v1/prometheusservicelevel.go b/pkg/kubernetes/gen/informers/externalversions/sloth/v1/prometheusservicelevel.go index d8605c40..786e5187 100644 --- a/pkg/kubernetes/gen/informers/externalversions/sloth/v1/prometheusservicelevel.go +++ b/pkg/kubernetes/gen/informers/externalversions/sloth/v1/prometheusservicelevel.go @@ -46,13 +46,25 @@ func NewFilteredPrometheusServiceLevelInformer(client versioned.Interface, names if tweakListOptions != nil { tweakListOptions(&options) } - return client.SlothV1().PrometheusServiceLevels(namespace).List(context.TODO(), options) + return client.SlothV1().PrometheusServiceLevels(namespace).List(context.Background(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.SlothV1().PrometheusServiceLevels(namespace).Watch(context.TODO(), options) + return client.SlothV1().PrometheusServiceLevels(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SlothV1().PrometheusServiceLevels(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.SlothV1().PrometheusServiceLevels(namespace).Watch(ctx, options) }, }, &apislothv1.PrometheusServiceLevel{}, diff --git a/scripts/kubegen.sh b/scripts/kubegen.sh index a47c3fc3..c345e473 100755 --- a/scripts/kubegen.sh +++ b/scripts/kubegen.sh @@ -3,7 +3,7 @@ set -o errexit set -o nounset -IMAGE_GEN=ghcr.io/slok/kube-code-generator:v0.6.0 +IMAGE_GEN=ghcr.io/slok/kube-code-generator:v0.7.0 GEN_DIRECTORY="pkg/kubernetes/gen" echo "Cleaning gen directory" From 48fb948497075c70faf73890ecf9d98aa56af9a7 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Tue, 27 May 2025 21:08:02 +0200 Subject: [PATCH 069/173] Update prom operator dependencies Signed-off-by: Xabier Larrakoetxea --- go.mod | 14 +++++++------- go.sum | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index d40d6cb3..8b16e5d3 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/alecthomas/kingpin/v2 v2.4.0 github.com/caarlos0/env/v11 v11.3.1 github.com/oklog/run v1.1.0 - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.0 - github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0 + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.2 + github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.2 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/common v0.64.0 github.com/prometheus/prometheus v0.304.0 @@ -59,9 +59,9 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.40.0 // indirect @@ -75,11 +75,11 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.32.3 // indirect + k8s.io/apiextensions-apiserver v0.33.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect - sigs.k8s.io/controller-runtime v0.20.4 // indirect + sigs.k8s.io/controller-runtime v0.21.0 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index 2db2a458..72d94faa 100644 --- a/go.sum +++ b/go.sum @@ -128,8 +128,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.0 h1:Ee6zu4IR/WKYEcYHL4+gbC1A3GAzlHWxSjjMyRVBHYw= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.0/go.mod h1:hY5yoQsoIalncoxYqXXCDL5y7f+GGYYlW9Bi2IdU5KY= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.2 h1:XXoaK87apyXqLm7xVEQ63pk8+GDupbVtHaBiW8IxPow= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.2/go.mod h1:hY5yoQsoIalncoxYqXXCDL5y7f+GGYYlW9Bi2IdU5KY= github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0 h1:g/wIdMrRyQ1Fac8plyQMXzky8R9kOi243tQi3a2YkyU= github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0/go.mod h1:yEp9v3FEYT+iR2ujaXFVS8ugTIQk0Mx+wwXGXoF5az8= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.2 h1:b1hxYDNCPjVtj7v2TfRxKSIJaX4pfV/Y3j5YF1VO4g4= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.2/go.mod h1:v0FyOD1KpmiRrecXU5X1OA0tGuAzFPzzIZ3GYLk3NXY= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -179,10 +183,16 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRND go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -258,10 +268,13 @@ k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= +k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= +k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= @@ -270,6 +283,8 @@ k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97 k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= From b8222a12eac4375255df4987baa587165c66b275 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 07:40:54 +0000 Subject: [PATCH 070/173] build(deps): bump github.com/prometheus/prometheus Bumps [github.com/prometheus/prometheus](https://github.com/prometheus/prometheus) from 0.304.0 to 0.304.1. - [Release notes](https://github.com/prometheus/prometheus/releases) - [Changelog](https://github.com/prometheus/prometheus/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/prometheus/compare/v0.304.0...v0.304.1) --- updated-dependencies: - dependency-name: github.com/prometheus/prometheus dependency-version: 0.304.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 19 ++----------------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 8b16e5d3..35cc9fd4 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.2 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/common v0.64.0 - github.com/prometheus/prometheus v0.304.0 + github.com/prometheus/prometheus v0.304.1 github.com/sirupsen/logrus v1.9.3 github.com/slok/reload v0.2.0 github.com/spotahome/kooper/v2 v2.9.0 diff --git a/go.sum b/go.sum index 72d94faa..feea944f 100644 --- a/go.sum +++ b/go.sum @@ -126,12 +126,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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-operator/prometheus-operator/pkg/apis/monitoring v0.82.0 h1:Ee6zu4IR/WKYEcYHL4+gbC1A3GAzlHWxSjjMyRVBHYw= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.0/go.mod h1:hY5yoQsoIalncoxYqXXCDL5y7f+GGYYlW9Bi2IdU5KY= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.2 h1:XXoaK87apyXqLm7xVEQ63pk8+GDupbVtHaBiW8IxPow= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.2/go.mod h1:hY5yoQsoIalncoxYqXXCDL5y7f+GGYYlW9Bi2IdU5KY= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0 h1:g/wIdMrRyQ1Fac8plyQMXzky8R9kOi243tQi3a2YkyU= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.0/go.mod h1:yEp9v3FEYT+iR2ujaXFVS8ugTIQk0Mx+wwXGXoF5az8= github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.2 h1:b1hxYDNCPjVtj7v2TfRxKSIJaX4pfV/Y3j5YF1VO4g4= github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.2/go.mod h1:v0FyOD1KpmiRrecXU5X1OA0tGuAzFPzzIZ3GYLk3NXY= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= @@ -142,8 +138,8 @@ github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQP github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/prometheus/prometheus v0.304.0 h1:otXBqfF7bbTcW7IrXrB6HMjo4dThQbayCPFr2yTlqrQ= -github.com/prometheus/prometheus v0.304.0/go.mod h1:ioGx2SGKTY+fLnJSQCdTHqARVldGNS8OlIe3kvp98so= +github.com/prometheus/prometheus v0.304.1 h1:e4kpJMb2Vh/PcR6LInake+ofcvFYHT+bCfmBvOkaZbY= +github.com/prometheus/prometheus v0.304.1/go.mod h1:ioGx2SGKTY+fLnJSQCdTHqARVldGNS8OlIe3kvp98so= github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8= github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAoaohfjqMU= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -181,16 +177,10 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -266,23 +256,18 @@ 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.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= -k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= -sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= From b16fe46b23b7a5a8991405e4b688ad6391958c0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 07:14:15 +0000 Subject: [PATCH 071/173] build(deps): bump golang in /docker/prod Bumps golang from 1.24.3-alpine to 1.24.4-alpine. --- updated-dependencies: - dependency-name: golang dependency-version: 1.24.4-alpine dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docker/prod/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index 4ffea32b..afc224b7 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,7 +1,7 @@ # Set also `ARCH` ARG here so we can use it on all the `FROM`s. ARG ARCH -FROM golang:1.24.3-alpine as build-stage +FROM golang:1.24.4-alpine as build-stage LABEL org.opencontainers.image.source=https://github.com/slok/sloth From 673e1fa7833a6d802a7eef46b841d8b38cec136a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Jun 2025 07:42:36 +0000 Subject: [PATCH 072/173] build(deps): bump github.com/prometheus/common from 0.64.0 to 0.65.0 Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.64.0 to 0.65.0. - [Release notes](https://github.com/prometheus/common/releases) - [Changelog](https://github.com/prometheus/common/blob/main/RELEASE.md) - [Commits](https://github.com/prometheus/common/compare/v0.64.0...v0.65.0) --- updated-dependencies: - dependency-name: github.com/prometheus/common dependency-version: 0.65.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 35cc9fd4..96794e57 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.2 github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.2 github.com/prometheus/client_golang v1.22.0 - github.com/prometheus/common v0.64.0 + github.com/prometheus/common v0.65.0 github.com/prometheus/prometheus v0.304.1 github.com/sirupsen/logrus v1.9.3 github.com/slok/reload v0.2.0 diff --git a/go.sum b/go.sum index feea944f..bdd78bbb 100644 --- a/go.sum +++ b/go.sum @@ -134,8 +134,8 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= -github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/prometheus v0.304.1 h1:e4kpJMb2Vh/PcR6LInake+ofcvFYHT+bCfmBvOkaZbY= From d3ab3a7ac8e50445a604a05f1f6a1b9aa178f75c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 09:03:18 +0000 Subject: [PATCH 073/173] build(deps): bump github.com/oklog/run from 1.1.0 to 1.2.0 Bumps [github.com/oklog/run](https://github.com/oklog/run) from 1.1.0 to 1.2.0. - [Release notes](https://github.com/oklog/run/releases) - [Commits](https://github.com/oklog/run/compare/v1.1.0...v1.2.0) --- updated-dependencies: - dependency-name: github.com/oklog/run dependency-version: 1.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 96794e57..f756e165 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/OpenSLO/oslo v0.12.0 github.com/alecthomas/kingpin/v2 v2.4.0 github.com/caarlos0/env/v11 v11.3.1 - github.com/oklog/run v1.1.0 + github.com/oklog/run v1.2.0 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.2 github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.2 github.com/prometheus/client_golang v1.22.0 diff --git a/go.sum b/go.sum index bdd78bbb..e63c2470 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= -github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= +github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= From 16be379d75693eca95861c681664104620b06ddc Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 6 Sep 2025 11:30:07 +0200 Subject: [PATCH 074/173] Update deps and add support for go 1.25 and k8s 1.34 Signed-off-by: Xabier Larrakoetxea --- .github/workflows/ci.yaml | 32 +-- .github/workflows/close-stale.yaml | 2 +- .github/workflows/generate.yaml | 2 +- .github/workflows/helmrelease.yaml | 2 +- .golangci.yml | 35 ++- CHANGELOG.md | 3 +- README.md | 2 +- cmd/sloth/commands/helpers.go | 6 +- cmd/sloth/commands/validate.go | 12 +- docker/dev/Dockerfile | 6 +- docker/prod/Dockerfile | 2 +- examples/_gen/k8s-getting-started.yml | 1 - examples/_gen/k8s-home-wifi.yml | 1 - examples/_gen/k8s-multifile.yml | 2 - examples/_gen/plugin-k8s-getting-started.yml | 1 - .../_gen/slo-plugin-k8s-getting-started.yml | 1 - go.mod | 92 +++--- go.sum | 264 ++++++++++-------- internal/app/generate/generatemock/mocks.go | 17 +- internal/app/kubecontroller/handler.go | 3 +- internal/storage/fs/fsmock/mocks.go | 34 ++- .../storage/io/prometheus_operator_test.go | 4 - internal/storage/k8s/k8s_test.go | 3 +- pkg/common/validation/promql.go | 5 +- .../applyconfiguration/internal/internal.go | 2 +- .../sloth/v1/prometheusservicelevel.go | 17 ++ .../gen/applyconfiguration/utils.go | 6 +- .../versioned/fake/clientset_generated.go | 4 +- scripts/kubegen.sh | 2 +- .../crd/prometheus-operator-crd.yaml | 1 - .../prometheus/testdata/out-base-k8s.yaml.tpl | 1 - .../testdata/out-multifile-k8s.yaml.tpl | 2 - .../testdata/out-slo-plugin-k8s.yaml.tpl | 1 - 33 files changed, 334 insertions(+), 234 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a8aed4ab..97bf8268 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,9 +7,9 @@ jobs: name: Check runs-on: ubuntu-latest # Execute the checks inside the container instead the VM. - container: golangci/golangci-lint:v1.64.8-alpine + container: golangci/golangci-lint:v2.4.0-alpine steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - run: | # We need this go flag because it started to error after golangci-lint is using Go 1.21. # TODO(slok): Remove it on next (>1.54.1) golangci-lint upgrade to check if this problem has gone. @@ -20,8 +20,8 @@ jobs: name: Unit test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod - run: make ci-test @@ -35,13 +35,13 @@ jobs: name: Helm chart test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Execute tests env: - HELM_VERSION: v3.6.3 + HELM_VERSION: v3.17.0 run: | # Get dependencies. echo "Getting dependencies..." @@ -52,8 +52,8 @@ jobs: name: Integration test CLI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Execute tests @@ -72,15 +72,15 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - kubernetes: [1.30.13, 1.31.9, 1.32.5, 1.33.1] + kubernetes: [1.31.12, 1.32.8, 1.33.4, 1.34.0] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Execute tests env: - KIND_VERSION: v0.28.0 + KIND_VERSION: v0.30.0 run: | # Get dependencies. echo "Getting dependencies..." @@ -124,7 +124,7 @@ jobs: name: Release images runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Docker login run: docker login ghcr.io -u ${{ github.actor }} -p "${{ secrets.GITHUB_TOKEN }}" - name: Build and publish docker images @@ -147,7 +147,7 @@ jobs: runs-on: ubuntu-latest steps: - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} # Sets VERSION env var. - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Docker login run: docker login ghcr.io -u ${{ github.actor }} -p "${{ secrets.GITHUB_TOKEN }}" - name: Build and publish docker images @@ -168,7 +168,7 @@ jobs: runs-on: ubuntu-latest steps: - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} # Sets VERSION env var. - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Build binaries run: | mkdir -p ./bin diff --git a/.github/workflows/close-stale.yaml b/.github/workflows/close-stale.yaml index fd98131b..8efd77a8 100644 --- a/.github/workflows/close-stale.yaml +++ b/.github/workflows/close-stale.yaml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9 + - uses: actions/stale@v10 with: days-before-stale: 60 days-before-close: 15 diff --git a/.github/workflows/generate.yaml b/.github/workflows/generate.yaml index d8e4006e..6644187c 100644 --- a/.github/workflows/generate.yaml +++ b/.github/workflows/generate.yaml @@ -11,7 +11,7 @@ jobs: name: Generate the SLOs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: download and setup generator binary run: | wget https://github.com/slok/sloth/releases/download/v0.9.0/sloth-linux-amd64 diff --git a/.github/workflows/helmrelease.yaml b/.github/workflows/helmrelease.yaml index da58ffd8..e3437ea8 100644 --- a/.github/workflows/helmrelease.yaml +++ b/.github/workflows/helmrelease.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.golangci.yml b/.golangci.yml index 95b937c6..23629459 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,21 +1,30 @@ ---- +version: "2" run: - timeout: 3m build-tags: - integration - linters: enable: + - godot - misspell - - goimports - revive + settings: + revive: + rules: + # Spammy linter and complex to fix on lots of parameters. Makes more harm that it solves. + - name: unused-parameter + disabled: true + staticcheck: + checks: + - all + # Omit embedded fields from selector expression. + # https://staticcheck.dev/docs/checks/#QF1008 + - -QF1008 + exclusions: + generated: lax + presets: + - comments + - std-error-handling +formatters: + enable: - gofmt - #- depguard - - godot - -linters-settings: - revive: - rules: - # Spammy linter and complex to fix on lots of parameters. Makes more harm that it solves. - - name: unused-parameter - disabled: true + - goimports diff --git a/CHANGELOG.md b/CHANGELOG.md index ec59b12a..31626b2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ - (BREAKING) `--sli-plugins-path`, `--slo-plugins-path`, `-m` args and it's env vars `SLOTH_SLI_PLUGINS_PATH`and `SLOTH_SLO_PLUGINS_PATH` have been removed in favor or `--plugins-path`, `-p` and it's env var `SLOTH_PLUGINS_PATH` that discovers and loads SLI and SLO plugins with a single flag. - Simplify validation and improve validation message by using custom logic instead of `go-playground/validator`. - (BREAKING) `--disable-optimized-rules` flag and associated env var has been removed. -- Update to Kubernetes v1.33. +- Update to Kubernetes v1.34. +- Update to Go v1.25. ### Added diff --git a/README.md b/README.md index 850d7d35..78a0b20d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/slok/sloth)](https://goreportcard.com/report/github.com/slok/sloth) [![Apache 2 licensed](https://img.shields.io/badge/license-Apache2-blue.svg)](https://raw.githubusercontent.com/slok/sloth/master/LICENSE) [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/slok/sloth)](https://github.com/slok/sloth/releases/latest) -![Kubernetes release](https://img.shields.io/badge/Kubernetes-v1.33-green?logo=Kubernetes&style=flat&color=326CE5&logoColor=white) +![Kubernetes release](https://img.shields.io/badge/Kubernetes-v1.34-green?logo=Kubernetes&style=flat&color=326CE5&logoColor=white) [![OpenSLO](https://img.shields.io/badge/OpenSLO-v1alpha-green?color=4974EA&style=flat)](https://github.com/OpenSLO/OpenSLO#slo) ## Project status diff --git a/cmd/sloth/commands/helpers.go b/cmd/sloth/commands/helpers.go index 37f38318..f9a578a7 100644 --- a/cmd/sloth/commands/helpers.go +++ b/cmd/sloth/commands/helpers.go @@ -133,8 +133,8 @@ func mapCmdPluginToModel(ctx context.Context, jsonPlugins []string) ([]model.Pro } func createDefaultSLOPlugins(logger log.Logger, disableRecordings, disableAlerts bool) ([]generate.SLOProcessor, error) { - var sliRuleGen generate.SLOProcessor = generate.NoopPlugin - var metaRuleGen generate.SLOProcessor = generate.NoopPlugin + sliRuleGen := generate.NoopPlugin + metaRuleGen := generate.NoopPlugin if !disableRecordings { sliPlugin, err := generate.NewSLOProcessorFromSLOPluginV1( plugincoreslirulesv1.NewPlugin, @@ -167,7 +167,7 @@ func createDefaultSLOPlugins(logger log.Logger, disableRecordings, disableAlerts } // Disable alert rules if required. - var alertRuleGen generate.SLOProcessor = generate.NoopPlugin + alertRuleGen := generate.NoopPlugin if !disableAlerts { plugin, err := generate.NewSLOProcessorFromSLOPluginV1( plugincorealertrulesv1.NewPlugin, diff --git a/cmd/sloth/commands/validate.go b/cmd/sloth/commands/validate.go index 06628dcc..3a7d2de8 100644 --- a/cmd/sloth/commands/validate.go +++ b/cmd/sloth/commands/validate.go @@ -145,12 +145,12 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { if promErr == nil { err := gen.GeneratePrometheus(ctx, *slos, io.Discard) if err != nil { - validation.Errs = []error{fmt.Errorf("Could not generate Prometheus format rules: %w", err)} + validation.Errs = []error{fmt.Errorf("could not generate Prometheus format rules: %w", err)} } continue } - validation.Errs = []error{fmt.Errorf("Tried loading raw prometheus SLOs spec, it couldn't: %w", promErr)} + validation.Errs = []error{fmt.Errorf("tried loading raw prometheus SLOs spec, it couldn't: %w", promErr)} case kubeYAMLLoader.IsSpecType(ctx, dataB): sloGroup, k8sErr := kubeYAMLLoader.LoadSpec(ctx, dataB) @@ -162,22 +162,22 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { continue } - validation.Errs = []error{fmt.Errorf("Tried loading Kubernetes prometheus SLOs spec, it couldn't: %w", k8sErr)} + validation.Errs = []error{fmt.Errorf("tried loading Kubernetes prometheus SLOs spec, it couldn't: %w", k8sErr)} case openSLOYAMLLoader.IsSpecType(ctx, dataB): slos, openSLOErr := openSLOYAMLLoader.LoadSpec(ctx, dataB) if openSLOErr == nil { err := gen.GenerateOpenSLO(ctx, *slos, io.Discard) if err != nil { - validation.Errs = []error{fmt.Errorf("Could not generate OpenSLO format rules: %w", err)} + validation.Errs = []error{fmt.Errorf("could not generate OpenSLO format rules: %w", err)} } continue } - validation.Errs = []error{fmt.Errorf("Tried loading OpenSLO SLOs spec, it couldn't: %s", openSLOErr)} + validation.Errs = []error{fmt.Errorf("tried loading OpenSLO SLOs spec, it couldn't: %s", openSLOErr)} default: - validation.Errs = []error{fmt.Errorf("Unknown spec type")} + validation.Errs = []error{fmt.Errorf("unknown spec type")} } } diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index a11d422f..aa820052 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -1,9 +1,9 @@ -FROM golang:1.24 +FROM golang:1.25 LABEL org.opencontainers.image.source=https://github.com/slok/sloth -ARG GOLANGCI_LINT_VERSION="1.64.8" -ARG MOCKERY_VERSION="3.1.0" +ARG GOLANGCI_LINT_VERSION="2.4.0" +ARG MOCKERY_VERSION="3.5.4" ARG GOMARKDOC_VERSION="1.1.0" ARG HELM_VERSION="3.17.0" ARG YAEGI_VERSION="0.16.1" diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index afc224b7..e49ee3d4 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,7 +1,7 @@ # Set also `ARCH` ARG here so we can use it on all the `FROM`s. ARG ARCH -FROM golang:1.24.4-alpine as build-stage +FROM golang:1.25-alpine as build-stage LABEL org.opencontainers.image.source=https://github.com/slok/sloth diff --git a/examples/_gen/k8s-getting-started.yml b/examples/_gen/k8s-getting-started.yml index de14e579..0d1a7360 100644 --- a/examples/_gen/k8s-getting-started.yml +++ b/examples/_gen/k8s-getting-started.yml @@ -6,7 +6,6 @@ apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth diff --git a/examples/_gen/k8s-home-wifi.yml b/examples/_gen/k8s-home-wifi.yml index 1927ed78..be417e17 100644 --- a/examples/_gen/k8s-home-wifi.yml +++ b/examples/_gen/k8s-home-wifi.yml @@ -6,7 +6,6 @@ apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: - creationTimestamp: null labels: app: sloth app.kubernetes.io/component: SLO diff --git a/examples/_gen/k8s-multifile.yml b/examples/_gen/k8s-multifile.yml index 11890be2..59c165ca 100644 --- a/examples/_gen/k8s-multifile.yml +++ b/examples/_gen/k8s-multifile.yml @@ -6,7 +6,6 @@ apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth @@ -265,7 +264,6 @@ spec: apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth diff --git a/examples/_gen/plugin-k8s-getting-started.yml b/examples/_gen/plugin-k8s-getting-started.yml index c50c0737..2fc29b3b 100644 --- a/examples/_gen/plugin-k8s-getting-started.yml +++ b/examples/_gen/plugin-k8s-getting-started.yml @@ -6,7 +6,6 @@ apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: - creationTimestamp: null labels: app: sloth app.kubernetes.io/component: SLO diff --git a/examples/_gen/slo-plugin-k8s-getting-started.yml b/examples/_gen/slo-plugin-k8s-getting-started.yml index 1a7987c8..13c8c458 100644 --- a/examples/_gen/slo-plugin-k8s-getting-started.yml +++ b/examples/_gen/slo-plugin-k8s-getting-started.yml @@ -6,7 +6,6 @@ apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth diff --git a/go.mod b/go.mod index f756e165..c9a85748 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,27 @@ module github.com/slok/sloth -go 1.24.0 +go 1.25.0 require ( github.com/OpenSLO/oslo v0.12.0 github.com/alecthomas/kingpin/v2 v2.4.0 github.com/caarlos0/env/v11 v11.3.1 github.com/oklog/run v1.2.0 - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.2 - github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.2 - github.com/prometheus/client_golang v1.22.0 - github.com/prometheus/common v0.65.0 - github.com/prometheus/prometheus v0.304.1 + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.85.1-0.20250905145128-3d04de8b9cba + github.com/prometheus-operator/prometheus-operator/pkg/client v0.85.1-0.20250905145128-3d04de8b9cba + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/common v0.66.1 + github.com/prometheus/prometheus v0.305.0 github.com/sirupsen/logrus v1.9.3 github.com/slok/reload v0.2.0 github.com/spotahome/kooper/v2 v2.9.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/traefik/yaegi v0.16.1 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.33.1 - k8s.io/apimachinery v0.33.1 - k8s.io/client-go v0.33.1 - sigs.k8s.io/structured-merge-diff/v4 v4.7.0 + k8s.io/api v0.34.0 + k8s.io/apimachinery v0.34.0 + k8s.io/client-go v0.34.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 ) require ( @@ -31,56 +31,68 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect github.com/edsrzf/mmap-go v1.2.0 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect - github.com/fxamacker/cbor/v2 v2.8.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-openapi/jsonpointer v0.22.0 // indirect + github.com/go-openapi/jsonreference v0.21.1 // indirect + github.com/go-openapi/swag v0.24.1 // indirect + github.com/go-openapi/swag/cmdutils v0.24.0 // indirect + github.com/go-openapi/swag/conv v0.24.0 // indirect + github.com/go-openapi/swag/fileutils v0.24.0 // indirect + github.com/go-openapi/swag/jsonname v0.24.0 // indirect + github.com/go-openapi/swag/jsonutils v0.24.0 // indirect + github.com/go-openapi/swag/loading v0.24.0 // indirect + github.com/go-openapi/swag/mangling v0.24.0 // indirect + github.com/go-openapi/swag/netutils v0.24.0 // indirect + github.com/go-openapi/swag/stringutils v0.24.0 // indirect + github.com/go-openapi/swag/typeutils v0.24.0 // indirect + github.com/go-openapi/swag/yamlutils v0.24.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect + github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/time v0.11.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/time v0.12.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.33.1 // indirect + k8s.io/apiextensions-apiserver v0.34.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect - sigs.k8s.io/controller-runtime v0.21.0 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611 // indirect + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect + sigs.k8s.io/controller-runtime v0.22.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index e63c2470..03a05a70 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ -cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU= -cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= +cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= @@ -18,8 +18,32 @@ github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjH github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= -github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= -github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= +github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -36,25 +60,47 @@ github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= -github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= +github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= +github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= +github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= +github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= +github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= +github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= +github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= +github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= +github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= +github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= +github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= +github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= +github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= +github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= +github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= +github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -63,26 +109,23 @@ github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeD github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= -github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= -github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= -github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= +github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= +github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= 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/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= @@ -104,8 +147,9 @@ github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUt 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= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/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/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= @@ -113,43 +157,41 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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-operator/prometheus-operator/pkg/apis/monitoring v0.82.2 h1:XXoaK87apyXqLm7xVEQ63pk8+GDupbVtHaBiW8IxPow= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.82.2/go.mod h1:hY5yoQsoIalncoxYqXXCDL5y7f+GGYYlW9Bi2IdU5KY= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.2 h1:b1hxYDNCPjVtj7v2TfRxKSIJaX4pfV/Y3j5YF1VO4g4= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.82.2/go.mod h1:v0FyOD1KpmiRrecXU5X1OA0tGuAzFPzzIZ3GYLk3NXY= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.85.1-0.20250905145128-3d04de8b9cba h1:7AXX1aIvTfVgIZ602DKHIukPGEbmA6URI16iLHsx1Rw= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.85.1-0.20250905145128-3d04de8b9cba/go.mod h1:k94oCBI0HYfPrYJQtmuDIVOVC+2Tv1n5sdwfq6QLX6E= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.85.1-0.20250905145128-3d04de8b9cba h1:Y/jQl7HVjQKrr8m7ntSjuGmMAoRd3KvBhwvhZkyAcTU= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.85.1-0.20250905145128-3d04de8b9cba/go.mod h1:oKQxfOEQRyUzsktM+6dxb6Ux5gNL461vSTnSz7Rx18Q= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/prometheus/prometheus v0.304.1 h1:e4kpJMb2Vh/PcR6LInake+ofcvFYHT+bCfmBvOkaZbY= -github.com/prometheus/prometheus v0.304.1/go.mod h1:ioGx2SGKTY+fLnJSQCdTHqARVldGNS8OlIe3kvp98so= -github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8= -github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAoaohfjqMU= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/prometheus/prometheus v0.305.0 h1:UO/LsM32/E9yBDtvQj8tN+WwhbyWKR10lO35vmFLx0U= +github.com/prometheus/prometheus v0.305.0/go.mod h1:JG+jKIDUJ9Bn97anZiCjwCxRyAx+lpcEQ0QnZlUlbwY= +github.com/prometheus/sigv4 v0.2.0 h1:qDFKnHYFswJxdzGeRP63c4HlH3Vbn1Yf/Ao2zabtVXk= +github.com/prometheus/sigv4 v0.2.0/go.mod h1:D04rqmAaPPEUkjRQxGqjoxdyJuyCh6E0M18fZr0zBiE= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slok/reload v0.2.0 h1:8ezO7EsaYMUCX2g1ZAdHTxD0Mo9oDn2legZZBoFMUEI= github.com/slok/reload v0.2.0/go.mod h1:vyJiGsSZTUx08vDoEiVszBp8Jr+bsMwIK/U1pK7XTmI= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spotahome/kooper/v2 v2.9.0 h1:Iwk2eAZbp0M0Z4OZYkt12c7ENhyYe8byB0mtDvVYjq4= github.com/spotahome/kooper/v2 v2.9.0/go.mod h1:im2PUUOGti/fXq0tUZaowG6cWJvgzS9BlX4ipz34c/E= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -163,8 +205,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E= github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -175,23 +217,27 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -200,53 +246,52 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn 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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 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= -google.golang.org/api v0.230.0 h1:2u1hni3E+UXAXrONrrkfWpi/V6cyKVAbfGVeGtC3OxM= -google.golang.org/api v0.230.0/go.mod h1:aqvtoMk7YkiXx+6U12arQFExiRV9D/ekvMCwCd/TksQ= -google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= -google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/api v0.238.0 h1:+EldkglWIg/pWjkq97sd+XxH7PxakNYoe/rkSTbnvOs= +google.golang.org/api v0.238.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= +google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -254,28 +299,27 @@ 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.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= -k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= +k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= +k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= +k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= +k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= +k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= +k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= -k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= -sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611 h1:o4oKOsvSymDkZRsMAPZU7bRdwL+lPOK5VS10Dr1D6eg= +k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.22.0 h1:mTOfibb8Hxwpx3xEkR56i7xSjB+nH4hZG37SrlCY5e0= +sigs.k8s.io/controller-runtime v0.22.0/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/app/generate/generatemock/mocks.go b/internal/app/generate/generatemock/mocks.go index 39f73cca..90c05214 100644 --- a/internal/app/generate/generatemock/mocks.go +++ b/internal/app/generate/generatemock/mocks.go @@ -72,15 +72,26 @@ type SLOPluginGetter_GetSLOPlugin_Call struct { } // GetSLOPlugin is a helper method to define mock.On call -// - ctx -// - id +// - ctx context.Context +// - id string func (_e *SLOPluginGetter_Expecter) GetSLOPlugin(ctx interface{}, id interface{}) *SLOPluginGetter_GetSLOPlugin_Call { return &SLOPluginGetter_GetSLOPlugin_Call{Call: _e.mock.On("GetSLOPlugin", ctx, id)} } func (_c *SLOPluginGetter_GetSLOPlugin_Call) Run(run func(ctx context.Context, id string)) *SLOPluginGetter_GetSLOPlugin_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) }) return _c } diff --git a/internal/app/kubecontroller/handler.go b/internal/app/kubecontroller/handler.go index bccdae03..4e57493f 100644 --- a/internal/app/kubecontroller/handler.go +++ b/internal/app/kubecontroller/handler.go @@ -12,14 +12,13 @@ import ( "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/storage" - "github.com/slok/sloth/pkg/common/model" commonmodel "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" ) // SpecLoader Knows how to load a Kubernetes Spec into an app model. type SpecLoader interface { - LoadSpec(ctx context.Context, spec *slothv1.PrometheusServiceLevel) (*model.PromSLOGroup, error) + LoadSpec(ctx context.Context, spec *slothv1.PrometheusServiceLevel) (*commonmodel.PromSLOGroup, error) } // Generator Knows how to generate SLO prometheus rules from app SLO model. diff --git a/internal/storage/fs/fsmock/mocks.go b/internal/storage/fs/fsmock/mocks.go index e90548da..ff3ae6e0 100644 --- a/internal/storage/fs/fsmock/mocks.go +++ b/internal/storage/fs/fsmock/mocks.go @@ -73,15 +73,26 @@ type SLIPluginLoader_LoadRawSLIPlugin_Call struct { } // LoadRawSLIPlugin is a helper method to define mock.On call -// - ctx -// - src +// - ctx context.Context +// - src string func (_e *SLIPluginLoader_Expecter) LoadRawSLIPlugin(ctx interface{}, src interface{}) *SLIPluginLoader_LoadRawSLIPlugin_Call { return &SLIPluginLoader_LoadRawSLIPlugin_Call{Call: _e.mock.On("LoadRawSLIPlugin", ctx, src)} } func (_c *SLIPluginLoader_LoadRawSLIPlugin_Call) Run(run func(ctx context.Context, src string)) *SLIPluginLoader_LoadRawSLIPlugin_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) }) return _c } @@ -157,15 +168,26 @@ type SLOPluginLoader_LoadRawPlugin_Call struct { } // LoadRawPlugin is a helper method to define mock.On call -// - ctx -// - src +// - ctx context.Context +// - src string func (_e *SLOPluginLoader_Expecter) LoadRawPlugin(ctx interface{}, src interface{}) *SLOPluginLoader_LoadRawPlugin_Call { return &SLOPluginLoader_LoadRawPlugin_Call{Call: _e.mock.On("LoadRawPlugin", ctx, src)} } func (_c *SLOPluginLoader_LoadRawPlugin_Call) Run(run func(ctx context.Context, src string)) *SLOPluginLoader_LoadRawPlugin_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) }) return _c } diff --git a/internal/storage/io/prometheus_operator_test.go b/internal/storage/io/prometheus_operator_test.go index bb27b0ed..cf015429 100644 --- a/internal/storage/io/prometheus_operator_test.go +++ b/internal/storage/io/prometheus_operator_test.go @@ -67,7 +67,6 @@ kind: PrometheusRule metadata: annotations: ak1: av1 - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth @@ -118,7 +117,6 @@ kind: PrometheusRule metadata: annotations: ak1: av1 - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth @@ -169,7 +167,6 @@ kind: PrometheusRule metadata: annotations: ak1: av1 - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth @@ -279,7 +276,6 @@ kind: PrometheusRule metadata: annotations: ak1: av1 - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth diff --git a/internal/storage/k8s/k8s_test.go b/internal/storage/k8s/k8s_test.go index a134a04c..25b75d0f 100644 --- a/internal/storage/k8s/k8s_test.go +++ b/internal/storage/k8s/k8s_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" @@ -250,7 +249,7 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { if test.expErr { assert.Error(err) } else if assert.NoError(err) { - gotPromRules, err := promOpCli.MonitoringV1().PrometheusRules("").List(t.Context(), v1.ListOptions{}) + gotPromRules, err := promOpCli.MonitoringV1().PrometheusRules("").List(t.Context(), metav1.ListOptions{}) require.NoError(err) assert.Equal(test.expPromOperatorRules, gotPromRules.Items) diff --git a/pkg/common/validation/promql.go b/pkg/common/validation/promql.go index 6dc0e32f..cb24fbab 100644 --- a/pkg/common/validation/promql.go +++ b/pkg/common/validation/promql.go @@ -18,7 +18,8 @@ func (promQLDialectValidator) ValidateLabelKey(k string) error { if k == prommodel.MetricNameLabel { return fmt.Errorf("the label key %q is not allowed", prommodel.MetricNameLabel) } - if !prommodel.LabelName(k).IsValid() { + + if !prommodel.UTF8Validation.IsValidLabelName(k) { return fmt.Errorf("the label key %q is not valid", k) } @@ -38,7 +39,7 @@ func (promQLDialectValidator) ValidateLabelValue(k string) error { } func (promQLDialectValidator) ValidateAnnotationKey(k string) error { - if !prommodel.LabelName(k).IsValid() { + if !prommodel.UTF8Validation.IsValidLabelName(k) { return fmt.Errorf("the annotation key %q is not valid", k) } diff --git a/pkg/kubernetes/gen/applyconfiguration/internal/internal.go b/pkg/kubernetes/gen/applyconfiguration/internal/internal.go index e1fd6791..e9ad9b47 100644 --- a/pkg/kubernetes/gen/applyconfiguration/internal/internal.go +++ b/pkg/kubernetes/gen/applyconfiguration/internal/internal.go @@ -6,7 +6,7 @@ import ( fmt "fmt" sync "sync" - typed "sigs.k8s.io/structured-merge-diff/v4/typed" + typed "sigs.k8s.io/structured-merge-diff/v6/typed" ) func Parser() *typed.Parser { diff --git a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevel.go b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevel.go index f27db280..eeb3fdd6 100644 --- a/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevel.go +++ b/pkg/kubernetes/gen/applyconfiguration/sloth/v1/prometheusservicelevel.go @@ -27,6 +27,7 @@ func PrometheusServiceLevel(name, namespace string) *PrometheusServiceLevelApply b.WithAPIVersion("sloth.slok.dev/v1") return b } +func (b PrometheusServiceLevelApplyConfiguration) IsApplyConfiguration() {} // WithKind sets the Kind field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. @@ -202,8 +203,24 @@ func (b *PrometheusServiceLevelApplyConfiguration) WithStatus(value *PrometheusS return b } +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *PrometheusServiceLevelApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *PrometheusServiceLevelApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + // GetName retrieves the value of the Name field in the declarative configuration. func (b *PrometheusServiceLevelApplyConfiguration) GetName() *string { b.ensureObjectMetaApplyConfigurationExists() return b.ObjectMetaApplyConfiguration.Name } + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *PrometheusServiceLevelApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/pkg/kubernetes/gen/applyconfiguration/utils.go b/pkg/kubernetes/gen/applyconfiguration/utils.go index 314db108..396f31cb 100644 --- a/pkg/kubernetes/gen/applyconfiguration/utils.go +++ b/pkg/kubernetes/gen/applyconfiguration/utils.go @@ -8,7 +8,7 @@ import ( slothv1 "github.com/slok/sloth/pkg/kubernetes/gen/applyconfiguration/sloth/v1" runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" - testing "k8s.io/client-go/testing" + managedfields "k8s.io/apimachinery/pkg/util/managedfields" ) // ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no @@ -45,6 +45,6 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return nil } -func NewTypeConverter(scheme *runtime.Scheme) *testing.TypeConverter { - return &testing.TypeConverter{Scheme: scheme, TypeResolver: internal.Parser()} +func NewTypeConverter(scheme *runtime.Scheme) managedfields.TypeConverter { + return managedfields.NewSchemeTypeConverter(scheme, internal.Parser()) } diff --git a/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go b/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go index 875640d0..f4d407a6 100644 --- a/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go +++ b/pkg/kubernetes/gen/clientset/versioned/fake/clientset_generated.go @@ -89,8 +89,8 @@ func NewClientset(objects ...runtime.Object) *Clientset { cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { var opts metav1.ListOptions - if watchActcion, ok := action.(testing.WatchActionImpl); ok { - opts = watchActcion.ListOptions + if watchAction, ok := action.(testing.WatchActionImpl); ok { + opts = watchAction.ListOptions } gvr := action.GetResource() ns := action.GetNamespace() diff --git a/scripts/kubegen.sh b/scripts/kubegen.sh index c345e473..aca573ff 100755 --- a/scripts/kubegen.sh +++ b/scripts/kubegen.sh @@ -3,7 +3,7 @@ set -o errexit set -o nounset -IMAGE_GEN=ghcr.io/slok/kube-code-generator:v0.7.0 +IMAGE_GEN=ghcr.io/slok/kube-code-generator:v0.8.0 GEN_DIRECTORY="pkg/kubernetes/gen" echo "Cleaning gen directory" diff --git a/test/integration/crd/prometheus-operator-crd.yaml b/test/integration/crd/prometheus-operator-crd.yaml index cd8705a2..99c100bf 100644 --- a/test/integration/crd/prometheus-operator-crd.yaml +++ b/test/integration/crd/prometheus-operator-crd.yaml @@ -6,7 +6,6 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.4.1 - creationTimestamp: null name: prometheusrules.monitoring.coreos.com spec: group: monitoring.coreos.com diff --git a/test/integration/prometheus/testdata/out-base-k8s.yaml.tpl b/test/integration/prometheus/testdata/out-base-k8s.yaml.tpl index c9a3184a..b7de7ecc 100644 --- a/test/integration/prometheus/testdata/out-base-k8s.yaml.tpl +++ b/test/integration/prometheus/testdata/out-base-k8s.yaml.tpl @@ -6,7 +6,6 @@ apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth diff --git a/test/integration/prometheus/testdata/out-multifile-k8s.yaml.tpl b/test/integration/prometheus/testdata/out-multifile-k8s.yaml.tpl index 1ebd100c..9f7f4312 100644 --- a/test/integration/prometheus/testdata/out-multifile-k8s.yaml.tpl +++ b/test/integration/prometheus/testdata/out-multifile-k8s.yaml.tpl @@ -6,7 +6,6 @@ apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth @@ -411,7 +410,6 @@ spec: apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth diff --git a/test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl b/test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl index 5e1954fd..33a3d9e1 100644 --- a/test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl +++ b/test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl @@ -6,7 +6,6 @@ apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: - creationTimestamp: null labels: app.kubernetes.io/component: SLO app.kubernetes.io/managed-by: sloth From 327e2d1a30160e1ce8baf99424ebe558c029bb9e Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Wed, 10 Sep 2025 09:57:32 +0200 Subject: [PATCH 075/173] Prepare for v0.13.0 Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 3 ++- deploy/kubernetes/helm/sloth/templates/deployment.yaml | 3 --- .../helm/sloth/tests/testdata/output/deployment_custom.yaml | 1 - .../tests/testdata/output/deployment_custom_no_extras.yaml | 1 - .../tests/testdata/output/deployment_custom_slo_config.yaml | 1 - .../helm/sloth/tests/testdata/output/deployment_default.yaml | 2 +- deploy/kubernetes/helm/sloth/tests/values_test.go | 1 - deploy/kubernetes/helm/sloth/values.yaml | 3 +-- deploy/kubernetes/raw/sloth-with-common-plugins.yaml | 2 +- deploy/kubernetes/raw/sloth.yaml | 2 +- 10 files changed, 6 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31626b2a..4cefd749 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ - Core SLO validation and SLO rules generation migrated to SLO plugins. - (BREAKING) `--sli-plugins-path`, `--slo-plugins-path`, `-m` args and it's env vars `SLOTH_SLI_PLUGINS_PATH`and `SLOTH_SLO_PLUGINS_PATH` have been removed in favor or `--plugins-path`, `-p` and it's env var `SLOTH_PLUGINS_PATH` that discovers and loads SLI and SLO plugins with a single flag. - Simplify validation and improve validation message by using custom logic instead of `go-playground/validator`. -- (BREAKING) `--disable-optimized-rules` flag and associated env var has been removed. +- (BREAKING) `--disable-optimized-rules` flag and associated env var has been removed. +- (BREAKING) Helm chart has removed the option for disabling optimized rules. - Update to Kubernetes v1.34. - Update to Go v1.25. diff --git a/deploy/kubernetes/helm/sloth/templates/deployment.yaml b/deploy/kubernetes/helm/sloth/templates/deployment.yaml index e9da4d88..c19acd0b 100644 --- a/deploy/kubernetes/helm/sloth/templates/deployment.yaml +++ b/deploy/kubernetes/helm/sloth/templates/deployment.yaml @@ -61,9 +61,6 @@ spec: {{- with .Values.sloth.defaultSloPeriod }} - --default-slo-period={{ . }} {{- end }} - {{- if not .Values.sloth.optimizedRules }} - - --disable-optimized-rules - {{- end }} {{- if .Values.customSloConfig.enabled }} - --slo-period-windows-path={{ .Values.customSloConfig.path }} {{- end }} diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml index 1331a9d2..a32c5299 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml @@ -52,7 +52,6 @@ spec: - --extra-labels=k1=v1 - --extra-labels=k2=v2 - --plugins-path=/plugins - - --disable-optimized-rules - --logger=default ports: - containerPort: 8081 diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_no_extras.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_no_extras.yaml index c67e2204..fa52daab 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_no_extras.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_no_extras.yaml @@ -51,7 +51,6 @@ spec: - --label-selector=x=y,z!=y - --extra-labels=k1=v1 - --extra-labels=k2=v2 - - --disable-optimized-rules - --logger=default securityContext: allowPrivilegeEscalation: false diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_slo_config.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_slo_config.yaml index 10fb51fc..4a37ddef 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_slo_config.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom_slo_config.yaml @@ -52,7 +52,6 @@ spec: - --label-selector=x=y,z!=y - --extra-labels=k1=v1 - --extra-labels=k2=v2 - - --disable-optimized-rules - --slo-period-windows-path=/windows - --logger=default ports: diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml index 65091427..2c8bad81 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml @@ -32,7 +32,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.12.0 + image: ghcr.io/slok/sloth:v0.13.0 args: - kubernetes-controller - --plugins-path=/plugins diff --git a/deploy/kubernetes/helm/sloth/tests/values_test.go b/deploy/kubernetes/helm/sloth/tests/values_test.go index 9de85763..bab68fe4 100644 --- a/deploy/kubernetes/helm/sloth/tests/values_test.go +++ b/deploy/kubernetes/helm/sloth/tests/values_test.go @@ -27,7 +27,6 @@ func customValues() msi { "workers": 99, "labelSelector": `x=y,z!=y`, "namespace": "somens", - "optimizedRules": false, "extraLabels": msi{ "k1": "v1", "k2": "v2", diff --git a/deploy/kubernetes/helm/sloth/values.yaml b/deploy/kubernetes/helm/sloth/values.yaml index 9ed6f836..6557adff 100644 --- a/deploy/kubernetes/helm/sloth/values.yaml +++ b/deploy/kubernetes/helm/sloth/values.yaml @@ -6,7 +6,7 @@ labels: {} image: registry: ghcr.io # This field cannot be empty if global.imageRegistry is also empty. repository: slok/sloth - tag: v0.12.0 + tag: v0.13.0 # -- Container resources: requests and limits for CPU, Memory resources: @@ -28,7 +28,6 @@ sloth: namespace: "" # The namespace where sloth will the CRs to process. extraLabels: {} # Labels that will be added to all the generated SLO Rules. defaultSloPeriod: "" # The slo period used by sloth (e.g. 30d). - optimizedRules: true # Reduce prom load for calculating period window burnrates. debug: enabled: false # Could be: default or json diff --git a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml index 22503772..4af3b888 100644 --- a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml +++ b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml @@ -85,7 +85,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.12.0 + image: ghcr.io/slok/sloth:v0.13.0 args: - kubernetes-controller - --plugins-path=/plugins diff --git a/deploy/kubernetes/raw/sloth.yaml b/deploy/kubernetes/raw/sloth.yaml index f73316b1..a2e9c068 100644 --- a/deploy/kubernetes/raw/sloth.yaml +++ b/deploy/kubernetes/raw/sloth.yaml @@ -85,7 +85,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.12.0 + image: ghcr.io/slok/sloth:v0.13.0 args: - kubernetes-controller - --logger=default From 5ee38b3c56bc9f95a3eb09a3a7cb864900238336 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Wed, 10 Sep 2025 19:49:55 +0200 Subject: [PATCH 076/173] Bump v0.13.0 Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cefd749..57511b56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [v0.13.0] - 2025-09-10 + ### Changed - Split image registry and repository in Helm chart @@ -209,7 +211,8 @@ - Support raw query based SLI. - Kubernetes (prometheus-operator) CRD generation support. -[unreleased]: https://github.com/slok/sloth/compare/v0.12.0...HEAD +[unreleased]: https://github.com/slok/sloth/compare/v0.13.0...HEAD +[v0.13.0]: https://github.com/slok/sloth/compare/v0.12.0...v0.13.0 [v0.12.0]: https://github.com/slok/sloth/compare/v0.11.0...v0.12.0 [v0.11.0]: https://github.com/slok/sloth/compare/v0.10.0...v0.11.0 [v0.10.0]: https://github.com/slok/sloth/compare/v0.9.0...v0.10.0 From a62a8d96e19e6aeab0d54fe9f42ad1e36a76e929 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Wed, 10 Sep 2025 20:35:45 +0200 Subject: [PATCH 077/173] Release Helm chart for Sloth v0.13.0 Signed-off-by: Xabier Larrakoetxea --- deploy/kubernetes/helm/sloth/Chart.yaml | 2 +- deploy/kubernetes/raw/sloth-with-common-plugins.yaml | 12 ++++++------ deploy/kubernetes/raw/sloth.yaml | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/deploy/kubernetes/helm/sloth/Chart.yaml b/deploy/kubernetes/helm/sloth/Chart.yaml index 7a1afe0a..5979b073 100644 --- a/deploy/kubernetes/helm/sloth/Chart.yaml +++ b/deploy/kubernetes/helm/sloth/Chart.yaml @@ -4,4 +4,4 @@ description: Base chart for Sloth. type: application home: https://github.com/slok/sloth kubeVersion: ">= 1.19.0-0" -version: 0.12.1 +version: 0.13.0 diff --git a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml index 4af3b888..48590f5b 100644 --- a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml +++ b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -133,7 +133,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth diff --git a/deploy/kubernetes/raw/sloth.yaml b/deploy/kubernetes/raw/sloth.yaml index a2e9c068..13c3c06f 100644 --- a/deploy/kubernetes/raw/sloth.yaml +++ b/deploy/kubernetes/raw/sloth.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -108,7 +108,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.12.1 + helm.sh/chart: sloth-0.13.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth From b6233fc20c0eb41e3539b37947cf37a64536c4c3 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Tue, 16 Sep 2025 20:53:13 +0200 Subject: [PATCH 078/173] Add info_labels_v1 contrib SLO plugin Signed-off-by: Xabier Larrakoetxea --- .github/CODEOWNERS | 6 + CHANGELOG.md | 5 + examples/_gen/contrib-slo-plugins.yml | 252 ++++++++++++++++++ examples/contrib-slo-plugins.yml | 38 +++ .../slo/contrib/info_labels_v1/README.md | 41 +++ .../slo/contrib/info_labels_v1/plugin.go | 54 ++++ .../slo/contrib/info_labels_v1/plugin_test.go | 173 ++++++++++++ 7 files changed, 569 insertions(+) create mode 100644 examples/_gen/contrib-slo-plugins.yml create mode 100644 examples/contrib-slo-plugins.yml create mode 100644 internal/plugin/slo/contrib/info_labels_v1/README.md create mode 100644 internal/plugin/slo/contrib/info_labels_v1/plugin.go create mode 100644 internal/plugin/slo/contrib/info_labels_v1/plugin_test.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 609f0d42..c9b8129c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,8 @@ +# Default owners. * @slok +# Contrib SLO plugins default owners. +/internal/plugin/slo/contrib/ @slok + +# Specific contrib SLO owners. +/internal/plugin/slo/contrib/info_labels_v1/ @cxdy @slok @wbollock diff --git a/CHANGELOG.md b/CHANGELOG.md index 57511b56..3778fffc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Added + +- Add contrib plugin directory and CODEOWNERS policies. +- Contrib plugin: `info_labels_v1`. + ## [v0.13.0] - 2025-09-10 ### Changed diff --git a/examples/_gen/contrib-slo-plugins.yml b/examples/_gen/contrib-slo-plugins.yml new file mode 100644 index 00000000..a83da44c --- /dev/null +++ b/examples/_gen/contrib-slo-plugins.yml @@ -0,0 +1,252 @@ + +--- +# Code generated by Sloth (dev): https://github.com/slok/sloth. +# DO NOT EDIT. + +groups: +- name: sloth-slo-sli-recordings-myservice-requests-availability-contrib-plugin-info-labels-v1 + rules: + - record: slo:sli_error:ratio_rate5m + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[5m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[5m]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + sloth_window: 5m + tier: "2" + - record: slo:sli_error:ratio_rate30m + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[30m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[30m]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + sloth_window: 30m + tier: "2" + - record: slo:sli_error:ratio_rate1h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + sloth_window: 1h + tier: "2" + - record: slo:sli_error:ratio_rate2h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[2h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[2h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + sloth_window: 2h + tier: "2" + - record: slo:sli_error:ratio_rate6h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[6h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[6h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + sloth_window: 6h + tier: "2" + - record: slo:sli_error:ratio_rate1d + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1d]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + sloth_window: 1d + tier: "2" + - record: slo:sli_error:ratio_rate3d + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[3d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[3d]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + sloth_window: 3d + tier: "2" + - record: slo:sli_error:ratio_rate30d + expr: | + sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"}[30d]) + / ignoring (sloth_window) + count_over_time(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"}[30d]) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + sloth_window: 30d + tier: "2" +- name: sloth-slo-meta-recordings-myservice-requests-availability-contrib-plugin-info-labels-v1 + rules: + - record: slo:objective:ratio + expr: vector(0.9990000000000001) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + tier: "2" + - record: slo:error_budget:ratio + expr: vector(1-0.9990000000000001) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + tier: "2" + - record: slo:time_period:days + expr: vector(30) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + tier: "2" + - record: slo:current_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + tier: "2" + - record: slo:period_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate30d{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + tier: "2" + - record: slo:period_error_budget_remaining:ratio + expr: 1 - slo:period_burn_rate:ratio{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", + sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + tier: "2" + - record: sloth_slo_info + expr: vector(1) + labels: + cmd: examplesgen.sh + owner: myteam + plug-l1: in-k1 + plug-l2: in-k2 + plug-l3: in-k3 + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-info-labels-v1 + sloth_mode: cli-gen-prom + sloth_objective: "99.9" + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-info-labels-v1 + sloth_spec: prometheus/v1 + sloth_version: dev + tier: "2" +- name: sloth-slo-alerts-myservice-requests-availability-contrib-plugin-info-labels-v1 + rules: + - alert: MyServiceHighErrorRate + expr: | + ( + max(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} > (14.4 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1h{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} > (14.4 * 0.0009999999999999432)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate30m{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} > (6 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate6h{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} > (6 * 0.0009999999999999432)) without (sloth_window) + ) + labels: + category: availability + routing_key: myteam + severity: pageteam + sloth_severity: page + annotations: + summary: High error rate on 'myservice' requests responses + title: (page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. + - alert: MyServiceHighErrorRate + expr: | + ( + max(slo:sli_error:ratio_rate2h{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} > (3 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1d{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} > (3 * 0.0009999999999999432)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate6h{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} > (1 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate3d{sloth_id="myservice-requests-availability-contrib-plugin-info-labels-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-info-labels-v1"} > (1 * 0.0009999999999999432)) without (sloth_window) + ) + labels: + category: availability + severity: slack + slack_channel: '#alerts-myteam' + sloth_severity: ticket + annotations: + summary: High error rate on 'myservice' requests responses + title: (ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. diff --git a/examples/contrib-slo-plugins.yml b/examples/contrib-slo-plugins.yml new file mode 100644 index 00000000..7f0f12e0 --- /dev/null +++ b/examples/contrib-slo-plugins.yml @@ -0,0 +1,38 @@ +version: "prometheus/v1" +service: "myservice" +labels: + owner: "myteam" + repo: "myorg/myservice" + tier: "2" + +slos: + - name: "requests-availability-contrib-plugin-info-labels-v1" + objective: 99.9 + description: "Common SLO based on availability for HTTP request responses." + plugins: + chain: + - id: "sloth.dev/contrib/info_labels/v1" + config: + labels: + plug-l1: in-k1 + plug-l2: in-k2 + plug-l3: in-k3 + sli: + events: + error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) + total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) + alerting: + name: MyServiceHighErrorRate + labels: + category: "availability" + annotations: + # Overwrite default Sloth SLO alert summmary on ticket and page alerts. + summary: "High error rate on 'myservice' requests responses" + page_alert: + labels: + severity: pageteam + routing_key: myteam + ticket_alert: + labels: + severity: "slack" + slack_channel: "#alerts-myteam" diff --git a/internal/plugin/slo/contrib/info_labels_v1/README.md b/internal/plugin/slo/contrib/info_labels_v1/README.md new file mode 100644 index 00000000..69803a99 --- /dev/null +++ b/internal/plugin/slo/contrib/info_labels_v1/README.md @@ -0,0 +1,41 @@ +# sloth.dev/contrib/info_labels/v1 + +This plugin adds labels to the `info` metric created by Sloth metadata recording rules. + +## Config + +- `labels` (**Required**, `map[string]string`): The labels to be added to the metric. +- `metricName` (**Optional**, `string`): If you want to customize the info metric where the labels will be added, by default Sloth info metadata metric: `sloth_slo_info`. + +## Env vars + +None + +## Order requirement + +This plugin should run after metadata rules generation plugins. + +## Usage examples + +### Custom labels + +```yaml +chain: + - id: "sloth.dev/contrib/info_labels/v1" + config: + labels: + label_k_1: label_v_2 + label_k_3: label_v_4 +``` + +### Custom info name + +```yaml +chain: + - id: "sloth.dev/contrib/info_labels/v1" + config: + metricName: 🦥_info + labels: + label_k_1: label_v_2 + label_k_3: label_v_4 +``` diff --git a/internal/plugin/slo/contrib/info_labels_v1/plugin.go b/internal/plugin/slo/contrib/info_labels_v1/plugin.go new file mode 100644 index 00000000..b683ac74 --- /dev/null +++ b/internal/plugin/slo/contrib/info_labels_v1/plugin.go @@ -0,0 +1,54 @@ +package plugin + +import ( + "context" + "encoding/json" + "fmt" + + utilsdata "github.com/slok/sloth/pkg/common/utils/data" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/contrib/info_labels/v1" +) + +type Config struct { + Labels map[string]string `json:"labels,omitempty"` + MetricName string `json:"metricName,omitempty"` +} + +func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + config := Config{} + err := json.Unmarshal(configData, &config) + if err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + if config.MetricName == "" { + config.MetricName = "sloth_slo_info" // Default info label. + } + + if len(config.Labels) == 0 { + return nil, fmt.Errorf("at least one label is required") + } + + return plugin{config: config}, nil +} + +type plugin struct { + config Config +} + +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + for i, r := range result.SLORules.MetadataRecRules.Rules { + if r.Record == p.config.MetricName { + r.Labels = utilsdata.MergeLabels(r.Labels, p.config.Labels) + result.SLORules.MetadataRecRules.Rules[i] = r + break + } + } + + return nil +} diff --git a/internal/plugin/slo/contrib/info_labels_v1/plugin_test.go b/internal/plugin/slo/contrib/info_labels_v1/plugin_test.go new file mode 100644 index 00000000..2f9adf67 --- /dev/null +++ b/internal/plugin/slo/contrib/info_labels_v1/plugin_test.go @@ -0,0 +1,173 @@ +package plugin_test + +import ( + "encoding/json" + "testing" + + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/stretchr/testify/assert" + + plugin "github.com/slok/sloth/internal/plugin/slo/contrib/info_labels_v1" + "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" +) + +func MustJSONRawMessage(t *testing.T, v any) json.RawMessage { + j, err := json.Marshal(v) + if err != nil { + t.Fatalf("failed to marshal %T: %v", v, err) + } + return j +} + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + config json.RawMessage + req pluginslov1.Request + res pluginslov1.Result + expRes pluginslov1.Result + expLoadErr bool + expErr bool + }{ + "A config without labels should fail.": { + config: MustJSONRawMessage(t, plugin.Config{}), + expLoadErr: true, + }, + + "Adding info labels to default SLO info rule should add the labels.": { + config: MustJSONRawMessage(t, plugin.Config{Labels: map[string]string{ + "plugin_🦥_label_1": "🦥_1", + "plugin_🦥_label_2": "🦥_2", + "plugin_🦥_label_3": "🦥_3", + }}), + req: pluginslov1.Request{}, + res: pluginslov1.Result{ + SLORules: model.PromSLORules{MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + {Record: "something", Labels: map[string]string{ + "k1": "v1", + "k2": "v2", + }}, + {Record: "sloth_slo_info", Labels: map[string]string{ + "k1": "v1", + "k2": "v2", + }}, + }}}, + }, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + {Record: "something", Labels: map[string]string{ + "k1": "v1", + "k2": "v2", + }}, + {Record: "sloth_slo_info", Labels: map[string]string{ + "k1": "v1", + "k2": "v2", + "plugin_🦥_label_1": "🦥_1", + "plugin_🦥_label_2": "🦥_2", + "plugin_🦥_label_3": "🦥_3", + }}, + }}}, + }, + }, + + "Adding info labels to custom SLO name rule should add the labels to the custom metric name.": { + config: MustJSONRawMessage(t, plugin.Config{ + MetricName: "something", + Labels: map[string]string{ + "plugin_🦥_label_1": "🦥_1", + "plugin_🦥_label_2": "🦥_2", + "plugin_🦥_label_3": "🦥_3", + }}), + req: pluginslov1.Request{}, + res: pluginslov1.Result{ + SLORules: model.PromSLORules{MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + {Record: "something", Labels: map[string]string{ + "k1": "v1", + "k2": "v2", + }}, + {Record: "sloth_slo_info", Labels: map[string]string{ + "k1": "v1", + "k2": "v2", + }}, + }}}, + }, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + {Record: "something", Labels: map[string]string{ + "k1": "v1", + "k2": "v2", + "plugin_🦥_label_1": "🦥_1", + "plugin_🦥_label_2": "🦥_2", + "plugin_🦥_label_3": "🦥_3", + }}, + {Record: "sloth_slo_info", Labels: map[string]string{ + "k1": "v1", + "k2": "v2", + }}, + }}}, + }, + }, + + "Adding info labels to default SLO info rule when is missing should ignore label addition without error.": { + config: MustJSONRawMessage(t, plugin.Config{Labels: map[string]string{ + "plugin_🦥_label_1": "🦥_1", + "plugin_🦥_label_2": "🦥_2", + "plugin_🦥_label_3": "🦥_3", + }}), + req: pluginslov1.Request{}, + res: pluginslov1.Result{}, + expRes: pluginslov1.Result{}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: test.config}) + if test.expLoadErr { + assert.Error(err) + return + } + assert.NoError(err) + + err = plugin.ProcessSLO(t.Context(), &test.req, &test.res) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, test.res) + } + }) + } +} + +func BenchmarkPluginYaegi(b *testing.B) { + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{ + PluginConfiguration: []byte(`{"labels":{"plugin_🦥_label_1":"🦥_1"}}`), + }) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPluginGo(b *testing.B) { + plugin, err := plugin.NewPlugin([]byte(`{"labels":{"plugin_🦥_label_1":"🦥_1"}}`), pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} From e45056ab6ff994ada04e3cf79dda43513aa8a5da Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Thu, 18 Sep 2025 15:56:18 +0200 Subject: [PATCH 079/173] Add Victoria metrics SLO validator plugin Signed-off-by: Xabier Larrakoetxea --- .github/CODEOWNERS | 3 +- CHANGELOG.md | 4 +- examples/_gen/victoria-metrics.yml | 672 ++++++++++++++++++ examples/victoria-metrics.yml | 50 ++ go.mod | 4 + go.sum | 8 + .../validate_victoria_metrics_v1/README.md | 47 ++ .../validate_victoria_metrics_v1/plugin.go | 110 +++ .../plugin_test.go | 187 +++++ internal/pluginengine/slo/custom/custom.go | 15 +- .../github_com-VictoriaMetrics-metricsql.go | 59 ++ .../github_com-prometheus-common-model.go | 136 ++-- 12 files changed, 1225 insertions(+), 70 deletions(-) create mode 100644 examples/_gen/victoria-metrics.yml create mode 100644 examples/victoria-metrics.yml create mode 100644 internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md create mode 100644 internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go create mode 100644 internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin_test.go create mode 100644 internal/pluginengine/slo/custom/github_com-VictoriaMetrics-metricsql.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c9b8129c..c277bc1d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,4 +5,5 @@ /internal/plugin/slo/contrib/ @slok # Specific contrib SLO owners. -/internal/plugin/slo/contrib/info_labels_v1/ @cxdy @slok @wbollock +/internal/plugin/slo/contrib/info_labels_v1/ @cxdy @slok @wbollock +/internal/plugin/slo/contrib/validate_victoria_metrics_v1/ @slok diff --git a/CHANGELOG.md b/CHANGELOG.md index 3778fffc..cea800c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ ### Added - Add contrib plugin directory and CODEOWNERS policies. -- Contrib plugin: `info_labels_v1`. +- Contrib plugin: `/internal/plugin/slo/contrib/info_labels_v1/`. +- Allow `github.com/VictoriaMetrics/metricsql` module in SLO plugins. +- Contrib plugin: `sloth.dev/contrib/validate_victoria_metrics/v1`. ## [v0.13.0] - 2025-09-10 diff --git a/examples/_gen/victoria-metrics.yml b/examples/_gen/victoria-metrics.yml new file mode 100644 index 00000000..b50e561c --- /dev/null +++ b/examples/_gen/victoria-metrics.yml @@ -0,0 +1,672 @@ + +--- +# Code generated by Sloth (dev): https://github.com/slok/sloth. +# DO NOT EDIT. + +groups: +- name: sloth-slo-sli-recordings-foo-bar-grpc-latency-percentile-90 + rules: + - record: slo:sli_error:ratio_rate5m + expr: | + (1 - histogram_share(0.2, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[5m])))) + / + (vector(1)[5m]) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + sloth_window: 5m + target_grade: A + target_le: "0.2" + type: latency + - record: slo:sli_error:ratio_rate30m + expr: | + (1 - histogram_share(0.2, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[30m])))) + / + (vector(1)[30m]) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + sloth_window: 30m + target_grade: A + target_le: "0.2" + type: latency + - record: slo:sli_error:ratio_rate1h + expr: | + (1 - histogram_share(0.2, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[1h])))) + / + (vector(1)[1h]) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + sloth_window: 1h + target_grade: A + target_le: "0.2" + type: latency + - record: slo:sli_error:ratio_rate2h + expr: | + (1 - histogram_share(0.2, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[2h])))) + / + (vector(1)[2h]) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + sloth_window: 2h + target_grade: A + target_le: "0.2" + type: latency + - record: slo:sli_error:ratio_rate6h + expr: | + (1 - histogram_share(0.2, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[6h])))) + / + (vector(1)[6h]) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + sloth_window: 6h + target_grade: A + target_le: "0.2" + type: latency + - record: slo:sli_error:ratio_rate1d + expr: | + (1 - histogram_share(0.2, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[1d])))) + / + (vector(1)[1d]) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + sloth_window: 1d + target_grade: A + target_le: "0.2" + type: latency + - record: slo:sli_error:ratio_rate3d + expr: | + (1 - histogram_share(0.2, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[3d])))) + / + (vector(1)[3d]) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + sloth_window: 3d + target_grade: A + target_le: "0.2" + type: latency + - record: slo:sli_error:ratio_rate30d + expr: | + sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"}[30d]) + / ignoring (sloth_window) + count_over_time(slo:sli_error:ratio_rate5m{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"}[30d]) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + sloth_window: 30d + target_grade: A + target_le: "0.2" + type: latency +- name: sloth-slo-meta-recordings-foo-bar-grpc-latency-percentile-90 + rules: + - record: slo:objective:ratio + expr: vector(0.9) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + target_grade: A + target_le: "0.2" + type: latency + - record: slo:error_budget:ratio + expr: vector(1-0.9) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + target_grade: A + target_le: "0.2" + type: latency + - record: slo:time_period:days + expr: vector(30) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + target_grade: A + target_le: "0.2" + type: latency + - record: slo:current_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate5m{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + target_grade: A + target_le: "0.2" + type: latency + - record: slo:period_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate30d{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + target_grade: A + target_le: "0.2" + type: latency + - record: slo:period_error_budget_remaining:ratio + expr: 1 - slo:period_burn_rate:ratio{sloth_id="foo-bar-grpc-latency-percentile-90", + sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + target_grade: A + target_le: "0.2" + type: latency + - record: sloth_slo_info + expr: vector(1) + labels: + actual_grade: A + actual_le: "0.2" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-90 + sloth_mode: cli-gen-prom + sloth_objective: "90" + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-90 + sloth_spec: prometheus/v1 + sloth_version: dev + target_grade: A + target_le: "0.2" + type: latency +- name: sloth-slo-alerts-foo-bar-grpc-latency-percentile-90 + rules: + - alert: slo_foo-bar_grpc-latency-percentile-90_fail + expr: | + ( + max(slo:sli_error:ratio_rate5m{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} > (14.4 * 0.1)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1h{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} > (14.4 * 0.1)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate30m{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} > (6 * 0.1)) without (sloth_window) + and + max(slo:sli_error:ratio_rate6h{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} > (6 * 0.1)) without (sloth_window) + ) + labels: + objective: "90" + objective_reversed: "10" + sloth_severity: page + annotations: + summary: '{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn + rate is over expected.' + title: (page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. + - alert: slo_foo-bar_grpc-latency-percentile-90_fail + expr: | + ( + max(slo:sli_error:ratio_rate2h{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} > (3 * 0.1)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1d{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} > (3 * 0.1)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate6h{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} > (1 * 0.1)) without (sloth_window) + and + max(slo:sli_error:ratio_rate3d{sloth_id="foo-bar-grpc-latency-percentile-90", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-90"} > (1 * 0.1)) without (sloth_window) + ) + labels: + objective: "90" + objective_reversed: "10" + sloth_severity: ticket + annotations: + summary: '{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn + rate is over expected.' + title: (ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. +- name: sloth-slo-sli-recordings-foo-bar-grpc-latency-percentile-99 + rules: + - record: slo:sli_error:ratio_rate5m + expr: | + (1 - histogram_share(0.4, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[5m])))) + / + (vector(1)[5m]) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + sloth_window: 5m + target_grade: A + target_le: "0.4" + type: latency + - record: slo:sli_error:ratio_rate30m + expr: | + (1 - histogram_share(0.4, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[30m])))) + / + (vector(1)[30m]) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + sloth_window: 30m + target_grade: A + target_le: "0.4" + type: latency + - record: slo:sli_error:ratio_rate1h + expr: | + (1 - histogram_share(0.4, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[1h])))) + / + (vector(1)[1h]) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + sloth_window: 1h + target_grade: A + target_le: "0.4" + type: latency + - record: slo:sli_error:ratio_rate2h + expr: | + (1 - histogram_share(0.4, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[2h])))) + / + (vector(1)[2h]) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + sloth_window: 2h + target_grade: A + target_le: "0.4" + type: latency + - record: slo:sli_error:ratio_rate6h + expr: | + (1 - histogram_share(0.4, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[6h])))) + / + (vector(1)[6h]) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + sloth_window: 6h + target_grade: A + target_le: "0.4" + type: latency + - record: slo:sli_error:ratio_rate1d + expr: | + (1 - histogram_share(0.4, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[1d])))) + / + (vector(1)[1d]) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + sloth_window: 1d + target_grade: A + target_le: "0.4" + type: latency + - record: slo:sli_error:ratio_rate3d + expr: | + (1 - histogram_share(0.4, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[3d])))) + / + (vector(1)[3d]) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + sloth_window: 3d + target_grade: A + target_le: "0.4" + type: latency + - record: slo:sli_error:ratio_rate30d + expr: | + sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"}[30d]) + / ignoring (sloth_window) + count_over_time(slo:sli_error:ratio_rate5m{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"}[30d]) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + sloth_window: 30d + target_grade: A + target_le: "0.4" + type: latency +- name: sloth-slo-meta-recordings-foo-bar-grpc-latency-percentile-99 + rules: + - record: slo:objective:ratio + expr: vector(0.99) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + target_grade: A + target_le: "0.4" + type: latency + - record: slo:error_budget:ratio + expr: vector(1-0.99) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + target_grade: A + target_le: "0.4" + type: latency + - record: slo:time_period:days + expr: vector(30) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + target_grade: A + target_le: "0.4" + type: latency + - record: slo:current_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate5m{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + target_grade: A + target_le: "0.4" + type: latency + - record: slo:period_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate30d{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + target_grade: A + target_le: "0.4" + type: latency + - record: slo:period_error_budget_remaining:ratio + expr: 1 - slo:period_burn_rate:ratio{sloth_id="foo-bar-grpc-latency-percentile-99", + sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + target_grade: A + target_le: "0.4" + type: latency + - record: sloth_slo_info + expr: vector(1) + labels: + actual_grade: A + actual_le: "0.4" + application: slowpoke + cmd: examplesgen.sh + generated: "true" + owner: foo + repo: content/foo-bar + sloth_id: foo-bar-grpc-latency-percentile-99 + sloth_mode: cli-gen-prom + sloth_objective: "99" + sloth_service: foo-bar + sloth_slo: grpc-latency-percentile-99 + sloth_spec: prometheus/v1 + sloth_version: dev + target_grade: A + target_le: "0.4" + type: latency +- name: sloth-slo-alerts-foo-bar-grpc-latency-percentile-99 + rules: + - alert: slo_foo-bar_grpc-latency-percentile-99_fail + expr: | + ( + max(slo:sli_error:ratio_rate5m{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} > (14.4 * 0.01)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1h{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} > (14.4 * 0.01)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate30m{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} > (6 * 0.01)) without (sloth_window) + and + max(slo:sli_error:ratio_rate6h{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} > (6 * 0.01)) without (sloth_window) + ) + labels: + objective: "99" + objective_reversed: "1" + sloth_severity: page + annotations: + summary: '{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn + rate is over expected.' + title: (page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. + - alert: slo_foo-bar_grpc-latency-percentile-99_fail + expr: | + ( + max(slo:sli_error:ratio_rate2h{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} > (3 * 0.01)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1d{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} > (3 * 0.01)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate6h{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} > (1 * 0.01)) without (sloth_window) + and + max(slo:sli_error:ratio_rate3d{sloth_id="foo-bar-grpc-latency-percentile-99", sloth_service="foo-bar", sloth_slo="grpc-latency-percentile-99"} > (1 * 0.01)) without (sloth_window) + ) + labels: + objective: "99" + objective_reversed: "1" + sloth_severity: ticket + annotations: + summary: '{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn + rate is over expected.' + title: (ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. diff --git a/examples/victoria-metrics.yml b/examples/victoria-metrics.yml new file mode 100644 index 00000000..f1d598a1 --- /dev/null +++ b/examples/victoria-metrics.yml @@ -0,0 +1,50 @@ +version: prometheus/v1 +service: foo-bar +labels: + owner: foo + repo: content/foo-bar + generated: true + type: latency + application: slowpoke +slo_plugins: + overridePrevious: true + chain: + - id: sloth.dev/contrib/validate_victoria_metrics/v1 + - id: sloth.dev/core/sli_rules/v1 + - id: sloth.dev/core/metadata_rules/v1 + - id: sloth.dev/core/alert_rules/v1 +slos: +- name: grpc-latency-percentile-90 + objective: 90 + description: '"grpc" 90 percentile Latency SLO for grade "A"' + labels: + actual_grade: A + target_grade: A + actual_le: 0.2 + target_le: 0.2 + sli: + events: + error_query: 1 - histogram_share(0.2, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[{{.window}}]))) + total_query: vector(1)[{{.window}}] + alerting: + name: slo_foo-bar_grpc-latency-percentile-90_fail + labels: + objective: 90 + objective_reversed: 10 +- name: grpc-latency-percentile-99 + objective: 99 + description: '"grpc" 99 percentile Latency SLO for grade "A"' + labels: + actual_grade: A + target_grade: A + actual_le: 0.4 + target_le: 0.4 + sli: + events: + error_query: 1 - histogram_share(0.4, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[{{.window}}]))) + total_query: vector(1)[{{.window}}] + alerting: + name: slo_foo-bar_grpc-latency-percentile-99_fail + labels: + objective: 99 + objective_reversed: 1 \ No newline at end of file diff --git a/go.mod b/go.mod index c9a85748..4e830f86 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/OpenSLO/oslo v0.12.0 + github.com/VictoriaMetrics/metricsql v0.84.8 github.com/alecthomas/kingpin/v2 v2.4.0 github.com/caarlos0/env/v11 v11.3.1 github.com/oklog/run v1.2.0 @@ -25,6 +26,7 @@ require ( ) require ( + github.com/VictoriaMetrics/metrics v1.35.3 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -66,6 +68,8 @@ require ( github.com/prometheus/procfs v0.17.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/valyala/fastrand v1.1.0 // indirect + github.com/valyala/histogram v1.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/go.sum b/go.sum index 03a05a70..dc4646ff 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,10 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/OpenSLO/oslo v0.12.0 h1:0zdxMgFE59TUKpe/L4+5ujgmJZW/kAuCOJbyqrTX4lc= github.com/OpenSLO/oslo v0.12.0/go.mod h1:6jyOTkqBCdkgqLXJ6WtJj7+o0rSexl6YZbWwnxpGwtU= +github.com/VictoriaMetrics/metrics v1.35.3 h1:DrQBBAjTb24WFlGAV9dAQsPDmDRyqL63kZ1Yfc+SRkM= +github.com/VictoriaMetrics/metrics v1.35.3/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= +github.com/VictoriaMetrics/metricsql v0.84.8 h1:5JXrvPJiYkYNqJVT7+hMZmpAwRHd3txBdlVIw4rJ1VM= +github.com/VictoriaMetrics/metricsql v0.84.8/go.mod h1:d4EisFO6ONP/HIGDYTAtwrejJBBeKGQYiRl095bS4QQ= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= @@ -209,6 +213,10 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E= github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY= +github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= +github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= +github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= +github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= diff --git a/internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md new file mode 100644 index 00000000..2c8a4943 --- /dev/null +++ b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md @@ -0,0 +1,47 @@ +# sloth.dev/contrib/validate_victoria_metrics/v1 + +This plugin validates the SLO specification to ensure it is correct and well-formed according to the **Victoria metricsQL dialect**. This should be the **first plugin executed** by Sloth and acts as a safety check before any rules are generated or other plugins are run. + +By default SLoth comes with Prometheus PromQL dialect validator loaded, so this default plugins needs to be disabled before victoria metrics validator can be executed instead. + +## Config + +None + +## Env vars + +None + +## Order requirement + +This plugin must be placed **first** in the plugin chain to validate the SLO before any further processing is done. + +## Usage examples + +### Explicit usage as SLO group plugin + +Disables default plugins, sets this and configures again the generation plguins + +```yaml +slo_plugins: + overridePrevious: true + chain: + - id: sloth.dev/contrib/validate_victoria_metrics/v1 # Custom validation. + - id: sloth.dev/core/sli_rules/v1 # Default set again. + - id: sloth.dev/core/metadata_rules/v1 # Default set again. + - id: sloth.dev/core/alert_rules/v1 # Default set again. +``` + +### Explicit usage as app plugin + +Disable all default logic and set a new logic for all the Sloth app by setting the custom victoria metrics validator plugin and setting again default Sloth SLO generator plugins. + +```bash +sloth generate \ + -i ./examples/victoria-metrics.yml \ + --disable-default-slo-plugins \ + -s '{"id": "sloth.dev/contrib/validate_victoria_metrics/v1"}' \ + -s '{"id": "sloth.dev/core/sli_rules/v1"}' \ + -s '{"id": "sloth.dev/core/metadata_rules/v1"}' \ + -s '{"id": "sloth.dev/core/alert_rules/v1"}' +``` diff --git a/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go new file mode 100644 index 00000000..25dd6837 --- /dev/null +++ b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go @@ -0,0 +1,110 @@ +package plugin + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "text/template" + + "github.com/VictoriaMetrics/metricsql" + prommodel "github.com/prometheus/common/model" + + "github.com/slok/sloth/pkg/common/validation" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/contrib/validate_victoria_metrics/v1" +) + +func NewPlugin(_ json.RawMessage, appUtils pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + return plugin{ + appUtils: appUtils, + }, nil +} + +type plugin struct { + appUtils pluginslov1.AppUtils +} + +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + err := validation.ValidateSLO(request.SLO, VictoriaMetricsDialectValidator) + if err != nil { + return fmt.Errorf("invalid slo %q: %w", request.SLO.ID, err) + } + + return nil +} + +// VictoriaMetricsDialectValidator is the SLO flavour validator for victoria metrics backends dialect. +const VictoriaMetricsDialectValidator = victoriaMetricsDialectValidator(false) + +type victoriaMetricsDialectValidator bool + +func (victoriaMetricsDialectValidator) ValidateLabelKey(k string) error { + if k == prommodel.MetricNameLabel { + return fmt.Errorf("the label key %q is not allowed", prommodel.MetricNameLabel) + } + + if !prommodel.UTF8Validation.IsValidLabelName(k) { + return fmt.Errorf("the label key %q is not valid", k) + } + + return nil +} + +func (victoriaMetricsDialectValidator) ValidateLabelValue(k string) error { + if k == "" { + return fmt.Errorf("the label value is required") + } + + if !prommodel.LabelValue(k).IsValid() { + return fmt.Errorf("the label value %q is not valid", k) + } + + return nil +} + +func (victoriaMetricsDialectValidator) ValidateAnnotationKey(k string) error { + if !prommodel.UTF8Validation.IsValidLabelName(k) { + return fmt.Errorf("the annotation key %q is not valid", k) + } + + return nil +} + +func (victoriaMetricsDialectValidator) ValidateAnnotationValue(k string) error { + if k == "" { + return fmt.Errorf("the annotation value is required") + } + + return nil +} + +var promExprTplAllowedFakeData = map[string]string{"window": "1m"} + +func (victoriaMetricsDialectValidator) ValidateQueryExpression(queryExpression string) error { + if queryExpression == "" { + return fmt.Errorf("query is required") + } + + // The expressions set by users can have some allowed templated data. + // We are rendering the expression with fake data so prometheus can + // have a final expr and check if is correct. + tpl, err := template.New("expr").Parse(queryExpression) + if err != nil { + return err + } + + var tplB bytes.Buffer + err = tpl.Execute(&tplB, promExprTplAllowedFakeData) + if err != nil { + return err + } + + _, err = metricsql.Parse(tplB.String()) + + return err +} diff --git a/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin_test.go b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin_test.go new file mode 100644 index 00000000..f3cb3740 --- /dev/null +++ b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin_test.go @@ -0,0 +1,187 @@ +package plugin_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + plugin "github.com/slok/sloth/internal/plugin/slo/contrib/validate_victoria_metrics_v1" + "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" +) + +func getPromQLGoodSLO() model.PromSLO { + return model.PromSLO{ + ID: "slo1-id", + Name: "test.slo-0_1", + Service: "test-svc", + TimeWindow: 30 * 24 * time.Hour, + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp",code=~"Internal|Unavailable"}[{{ .window }}]))`, + TotalQuery: `sum(rate(grpc_server_handled_requests_count{job="myapp"}[{{ .window }}]))`, + }, + }, + Objective: 99.99, + Labels: map[string]string{ + "owner": "myteam", + "category": "test", + }, + PageAlertMeta: model.PromAlertMeta{ + Disable: false, + Name: "testAlert", + Labels: map[string]string{ + "tier": "1", + "severity": "slack", + "channel": "#a-myteam", + }, + Annotations: map[string]string{ + "message": "This is very important.", + "runbook": "http://whatever.com", + }, + }, + TicketAlertMeta: model.PromAlertMeta{ + Disable: false, + Name: "testAlert", + Labels: map[string]string{ + "tier": "1", + "severity": "slack", + "channel": "#a-not-so-important", + }, + Annotations: map[string]string{ + "message": "This is not very important.", + "runbook": "http://whatever.com", + }, + }, + } +} + +func getVictoriaMetricsQLGoodSLO() model.PromSLO { + return model.PromSLO{ + ID: "slo1-id", + Name: "test.slo-0_1", + Service: "test-svc", + TimeWindow: 30 * 24 * time.Hour, + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `1 - histogram_share(0.2, sum by (vmrange) (rate(requests_duration_seconds_bucket{container_name="foo-bar-grpc", agent_dc="dc_sl"}[{{.window}}])))`, + TotalQuery: `vector(1)[{{.window}}]`, + }, + }, + Objective: 99.99, + Labels: map[string]string{ + "owner": "myteam", + "category": "test", + }, + PageAlertMeta: model.PromAlertMeta{ + Disable: false, + Name: "testAlert", + Labels: map[string]string{ + "tier": "1", + "severity": "slack", + "channel": "#a-myteam", + }, + Annotations: map[string]string{ + "message": "This is very important.", + "runbook": "http://whatever.com", + }, + }, + TicketAlertMeta: model.PromAlertMeta{ + Disable: false, + Name: "testAlert", + Labels: map[string]string{ + "tier": "1", + "severity": "slack", + "channel": "#a-not-so-important", + }, + Annotations: map[string]string{ + "message": "This is not very important.", + "runbook": "http://whatever.com", + }, + }, + } +} + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + config json.RawMessage + req pluginslov1.Request + expRes pluginslov1.Result + expErr bool + }{ + "An invalid SLO should fail.": { + req: pluginslov1.Request{ + SLO: model.PromSLO{ + ID: "test", + }, + }, + expErr: true, + }, + + "A correct PromQL SLO should not fail.": { + req: pluginslov1.Request{ + SLO: getPromQLGoodSLO(), + }, + expRes: pluginslov1.Result{}, + }, + + "A correct victoria MetricsQL SLO should not fail.": { + req: pluginslov1.Request{ + SLO: getVictoriaMetricsQLGoodSLO(), + }, + expRes: pluginslov1.Result{}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: test.config}) + require.NoError(err) + + res := pluginslov1.Result{} + err = plugin.ProcessSLO(t.Context(), &test.req, &res) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, res) + } + }) + } +} + +func BenchmarkPluginYaegi(b *testing.B) { + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: []byte(`{}`)}) + if err != nil { + b.Fatal(err) + } + + slo := getVictoriaMetricsQLGoodSLO() + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{SLO: slo}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPluginGo(b *testing.B) { + plugin, err := plugin.NewPlugin([]byte(`{}`), pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } + + slo := getVictoriaMetricsQLGoodSLO() + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{SLO: slo}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/internal/pluginengine/slo/custom/custom.go b/internal/pluginengine/slo/custom/custom.go index 19e04d41..7368f5a0 100644 --- a/internal/pluginengine/slo/custom/custom.go +++ b/internal/pluginengine/slo/custom/custom.go @@ -6,9 +6,20 @@ import ( _ "github.com/caarlos0/env/v11" // Used only by yaegi plugins, not by Sloth. ) -//go:generate yaegi extract --name custom github.com/prometheus/common/model github.com/prometheus/prometheus/model/rulefmt github.com/prometheus/prometheus/promql/parser -//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/prometheus/plugin/slo/v1 github.com/slok/sloth/pkg/common/conventions github.com/slok/sloth/pkg/common/model github.com/slok/sloth/pkg/common/utils/data github.com/slok/sloth/pkg/common/utils/prometheus github.com/slok/sloth/pkg/common/validation //go:generate yaegi extract --name custom github.com/caarlos0/env/v11 +//go:generate yaegi extract --name custom github.com/prometheus/common/model +//go:generate yaegi extract --name custom github.com/prometheus/prometheus/model/rulefmt +//go:generate yaegi extract --name custom github.com/prometheus/prometheus/promql/parser + +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/prometheus/plugin/slo/v1 +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/conventions +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/model +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/utils/data +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/utils/prometheus +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/validation + +//go:generate yaegi extract --name custom github.com/VictoriaMetrics/metricsql + // Symbols variable stores the map of custom Yaegi symbols per package. var Symbols = map[string]map[string]reflect.Value{} diff --git a/internal/pluginengine/slo/custom/github_com-VictoriaMetrics-metricsql.go b/internal/pluginengine/slo/custom/github_com-VictoriaMetrics-metricsql.go new file mode 100644 index 00000000..4c6fd5d0 --- /dev/null +++ b/internal/pluginengine/slo/custom/github_com-VictoriaMetrics-metricsql.go @@ -0,0 +1,59 @@ +// Code generated by 'yaegi extract github.com/VictoriaMetrics/metricsql'. DO NOT EDIT. + +package custom + +import ( + "github.com/VictoriaMetrics/metricsql" + "reflect" +) + +func init() { + Symbols["github.com/VictoriaMetrics/metricsql/metricsql"] = map[string]reflect.Value{ + // function, constant and variable definitions + "Clone": reflect.ValueOf(metricsql.Clone), + "CompileRegexp": reflect.ValueOf(metricsql.CompileRegexp), + "CompileRegexpAnchored": reflect.ValueOf(metricsql.CompileRegexpAnchored), + "DurationValue": reflect.ValueOf(metricsql.DurationValue), + "ExpandWithExprs": reflect.ValueOf(metricsql.ExpandWithExprs), + "GetRollupArgIdx": reflect.ValueOf(metricsql.GetRollupArgIdx), + "IsAggrFunc": reflect.ValueOf(metricsql.IsAggrFunc), + "IsBinaryOpCmp": reflect.ValueOf(metricsql.IsBinaryOpCmp), + "IsLikelyInvalid": reflect.ValueOf(metricsql.IsLikelyInvalid), + "IsRollupFunc": reflect.ValueOf(metricsql.IsRollupFunc), + "IsSupportedFunction": reflect.ValueOf(metricsql.IsSupportedFunction), + "IsTransformFunc": reflect.ValueOf(metricsql.IsTransformFunc), + "Optimize": reflect.ValueOf(metricsql.Optimize), + "Parse": reflect.ValueOf(metricsql.Parse), + "PositiveDurationValue": reflect.ValueOf(metricsql.PositiveDurationValue), + "Prettify": reflect.ValueOf(metricsql.Prettify), + "PushdownBinaryOpFilters": reflect.ValueOf(metricsql.PushdownBinaryOpFilters), + "TrimFiltersByGroupModifier": reflect.ValueOf(metricsql.TrimFiltersByGroupModifier), + "VisitAll": reflect.ValueOf(metricsql.VisitAll), + + // type definitions + "AggrFuncExpr": reflect.ValueOf((*metricsql.AggrFuncExpr)(nil)), + "BinaryOpExpr": reflect.ValueOf((*metricsql.BinaryOpExpr)(nil)), + "DurationExpr": reflect.ValueOf((*metricsql.DurationExpr)(nil)), + "Expr": reflect.ValueOf((*metricsql.Expr)(nil)), + "FuncExpr": reflect.ValueOf((*metricsql.FuncExpr)(nil)), + "LabelFilter": reflect.ValueOf((*metricsql.LabelFilter)(nil)), + "MetricExpr": reflect.ValueOf((*metricsql.MetricExpr)(nil)), + "ModifierExpr": reflect.ValueOf((*metricsql.ModifierExpr)(nil)), + "NumberExpr": reflect.ValueOf((*metricsql.NumberExpr)(nil)), + "RollupExpr": reflect.ValueOf((*metricsql.RollupExpr)(nil)), + "StringExpr": reflect.ValueOf((*metricsql.StringExpr)(nil)), + + // interface wrapper definitions + "_Expr": reflect.ValueOf((*_github_com_VictoriaMetrics_metricsql_Expr)(nil)), + } +} + +// _github_com_VictoriaMetrics_metricsql_Expr is an interface wrapper for Expr type +type _github_com_VictoriaMetrics_metricsql_Expr struct { + IValue interface{} + WAppendString func(dst []byte) []byte +} + +func (W _github_com_VictoriaMetrics_metricsql_Expr) AppendString(dst []byte) []byte { + return W.WAppendString(dst) +} diff --git a/internal/pluginengine/slo/custom/github_com-prometheus-common-model.go b/internal/pluginengine/slo/custom/github_com-prometheus-common-model.go index 25cd0908..51c9fa20 100644 --- a/internal/pluginengine/slo/custom/github_com-prometheus-common-model.go +++ b/internal/pluginengine/slo/custom/github_com-prometheus-common-model.go @@ -12,72 +12,76 @@ import ( func init() { Symbols["github.com/prometheus/common/model/model"] = map[string]reflect.Value{ // function, constant and variable definitions - "AddressLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__address__\"", token.STRING, 0)), - "AlertFiring": reflect.ValueOf(model.AlertFiring), - "AlertNameLabel": reflect.ValueOf(constant.MakeFromLiteral("\"alertname\"", token.STRING, 0)), - "AlertResolved": reflect.ValueOf(model.AlertResolved), - "AllowUTF8": reflect.ValueOf(constant.MakeFromLiteral("\"allow-utf-8\"", token.STRING, 0)), - "BucketLabel": reflect.ValueOf(constant.MakeFromLiteral("\"le\"", token.STRING, 0)), - "DotsEscaping": reflect.ValueOf(model.DotsEscaping), - "Earliest": reflect.ValueOf(model.Earliest), - "EscapeDots": reflect.ValueOf(constant.MakeFromLiteral("\"dots\"", token.STRING, 0)), - "EscapeMetricFamily": reflect.ValueOf(model.EscapeMetricFamily), - "EscapeName": reflect.ValueOf(model.EscapeName), - "EscapeUnderscores": reflect.ValueOf(constant.MakeFromLiteral("\"underscores\"", token.STRING, 0)), - "EscapeValues": reflect.ValueOf(constant.MakeFromLiteral("\"values\"", token.STRING, 0)), - "EscapingKey": reflect.ValueOf(constant.MakeFromLiteral("\"escaping\"", token.STRING, 0)), - "ExportedLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"exported_\"", token.STRING, 0)), - "FingerprintFromString": reflect.ValueOf(model.FingerprintFromString), - "InstanceLabel": reflect.ValueOf(constant.MakeFromLiteral("\"instance\"", token.STRING, 0)), - "IsValidLegacyMetricName": reflect.ValueOf(model.IsValidLegacyMetricName), - "IsValidMetricName": reflect.ValueOf(model.IsValidMetricName), - "JobLabel": reflect.ValueOf(constant.MakeFromLiteral("\"job\"", token.STRING, 0)), - "LabelNameRE": reflect.ValueOf(&model.LabelNameRE).Elem(), - "LabelsToSignature": reflect.ValueOf(model.LabelsToSignature), - "Latest": reflect.ValueOf(model.Latest), - "LegacyValidation": reflect.ValueOf(model.LegacyValidation), - "MetaLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__meta_\"", token.STRING, 0)), - "MetricNameLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__name__\"", token.STRING, 0)), - "MetricNameRE": reflect.ValueOf(&model.MetricNameRE).Elem(), - "MetricTypeCounter": reflect.ValueOf(model.MetricTypeCounter), - "MetricTypeGauge": reflect.ValueOf(model.MetricTypeGauge), - "MetricTypeGaugeHistogram": reflect.ValueOf(model.MetricTypeGaugeHistogram), - "MetricTypeHistogram": reflect.ValueOf(model.MetricTypeHistogram), - "MetricTypeInfo": reflect.ValueOf(model.MetricTypeInfo), - "MetricTypeStateset": reflect.ValueOf(model.MetricTypeStateset), - "MetricTypeSummary": reflect.ValueOf(model.MetricTypeSummary), - "MetricTypeUnknown": reflect.ValueOf(model.MetricTypeUnknown), - "MetricsPathLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__metrics_path__\"", token.STRING, 0)), - "NameEscapingScheme": reflect.ValueOf(&model.NameEscapingScheme).Elem(), - "NameValidationScheme": reflect.ValueOf(&model.NameValidationScheme).Elem(), - "NoEscaping": reflect.ValueOf(model.NoEscaping), - "Now": reflect.ValueOf(model.Now), - "ParamLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__param_\"", token.STRING, 0)), - "ParseDuration": reflect.ValueOf(model.ParseDuration), - "ParseFingerprint": reflect.ValueOf(model.ParseFingerprint), - "QuantileLabel": reflect.ValueOf(constant.MakeFromLiteral("\"quantile\"", token.STRING, 0)), - "ReservedLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__\"", token.STRING, 0)), - "SchemeLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__scheme__\"", token.STRING, 0)), - "ScrapeIntervalLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__scrape_interval__\"", token.STRING, 0)), - "ScrapeTimeoutLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__scrape_timeout__\"", token.STRING, 0)), - "SeparatorByte": reflect.ValueOf(model.SeparatorByte), - "SignatureForLabels": reflect.ValueOf(model.SignatureForLabels), - "SignatureWithoutLabels": reflect.ValueOf(model.SignatureWithoutLabels), - "TimeFromUnix": reflect.ValueOf(model.TimeFromUnix), - "TimeFromUnixNano": reflect.ValueOf(model.TimeFromUnixNano), - "TmpLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__tmp_\"", token.STRING, 0)), - "ToEscapingScheme": reflect.ValueOf(model.ToEscapingScheme), - "UTF8Validation": reflect.ValueOf(model.UTF8Validation), - "UnderscoreEscaping": reflect.ValueOf(model.UnderscoreEscaping), - "UnescapeName": reflect.ValueOf(model.UnescapeName), - "ValMatrix": reflect.ValueOf(model.ValMatrix), - "ValNone": reflect.ValueOf(model.ValNone), - "ValScalar": reflect.ValueOf(model.ValScalar), - "ValString": reflect.ValueOf(model.ValString), - "ValVector": reflect.ValueOf(model.ValVector), - "ValueEncodingEscaping": reflect.ValueOf(model.ValueEncodingEscaping), - "ZeroSample": reflect.ValueOf(&model.ZeroSample).Elem(), - "ZeroSamplePair": reflect.ValueOf(&model.ZeroSamplePair).Elem(), + "AddressLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__address__\"", token.STRING, 0)), + "AlertFiring": reflect.ValueOf(model.AlertFiring), + "AlertNameLabel": reflect.ValueOf(constant.MakeFromLiteral("\"alertname\"", token.STRING, 0)), + "AlertResolved": reflect.ValueOf(model.AlertResolved), + "AllowUTF8": reflect.ValueOf(constant.MakeFromLiteral("\"allow-utf-8\"", token.STRING, 0)), + "BucketLabel": reflect.ValueOf(constant.MakeFromLiteral("\"le\"", token.STRING, 0)), + "DotsEscaping": reflect.ValueOf(model.DotsEscaping), + "Earliest": reflect.ValueOf(model.Earliest), + "EscapeDots": reflect.ValueOf(constant.MakeFromLiteral("\"dots\"", token.STRING, 0)), + "EscapeMetricFamily": reflect.ValueOf(model.EscapeMetricFamily), + "EscapeName": reflect.ValueOf(model.EscapeName), + "EscapeUnderscores": reflect.ValueOf(constant.MakeFromLiteral("\"underscores\"", token.STRING, 0)), + "EscapeValues": reflect.ValueOf(constant.MakeFromLiteral("\"values\"", token.STRING, 0)), + "EscapingKey": reflect.ValueOf(constant.MakeFromLiteral("\"escaping\"", token.STRING, 0)), + "ExportedLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"exported_\"", token.STRING, 0)), + "FingerprintFromString": reflect.ValueOf(model.FingerprintFromString), + "InstanceLabel": reflect.ValueOf(constant.MakeFromLiteral("\"instance\"", token.STRING, 0)), + "IsValidLegacyMetricName": reflect.ValueOf(model.IsValidLegacyMetricName), + "IsValidMetricName": reflect.ValueOf(model.IsValidMetricName), + "JobLabel": reflect.ValueOf(constant.MakeFromLiteral("\"job\"", token.STRING, 0)), + "LabelNameRE": reflect.ValueOf(&model.LabelNameRE).Elem(), + "LabelsToSignature": reflect.ValueOf(model.LabelsToSignature), + "Latest": reflect.ValueOf(model.Latest), + "LegacyValidation": reflect.ValueOf(model.LegacyValidation), + "MetaLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__meta_\"", token.STRING, 0)), + "MetricNameLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__name__\"", token.STRING, 0)), + "MetricNameRE": reflect.ValueOf(&model.MetricNameRE).Elem(), + "MetricTypeCounter": reflect.ValueOf(model.MetricTypeCounter), + "MetricTypeGauge": reflect.ValueOf(model.MetricTypeGauge), + "MetricTypeGaugeHistogram": reflect.ValueOf(model.MetricTypeGaugeHistogram), + "MetricTypeHistogram": reflect.ValueOf(model.MetricTypeHistogram), + "MetricTypeInfo": reflect.ValueOf(model.MetricTypeInfo), + "MetricTypeLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__type__\"", token.STRING, 0)), + "MetricTypeStateset": reflect.ValueOf(model.MetricTypeStateset), + "MetricTypeSummary": reflect.ValueOf(model.MetricTypeSummary), + "MetricTypeUnknown": reflect.ValueOf(model.MetricTypeUnknown), + "MetricUnitLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__unit__\"", token.STRING, 0)), + "MetricsPathLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__metrics_path__\"", token.STRING, 0)), + "NameEscapingScheme": reflect.ValueOf(&model.NameEscapingScheme).Elem(), + "NameValidationScheme": reflect.ValueOf(&model.NameValidationScheme).Elem(), + "NoEscaping": reflect.ValueOf(model.NoEscaping), + "Now": reflect.ValueOf(model.Now), + "ParamLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__param_\"", token.STRING, 0)), + "ParseDuration": reflect.ValueOf(model.ParseDuration), + "ParseDurationAllowNegative": reflect.ValueOf(model.ParseDurationAllowNegative), + "ParseFingerprint": reflect.ValueOf(model.ParseFingerprint), + "QuantileLabel": reflect.ValueOf(constant.MakeFromLiteral("\"quantile\"", token.STRING, 0)), + "ReservedLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__\"", token.STRING, 0)), + "SchemeLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__scheme__\"", token.STRING, 0)), + "ScrapeIntervalLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__scrape_interval__\"", token.STRING, 0)), + "ScrapeTimeoutLabel": reflect.ValueOf(constant.MakeFromLiteral("\"__scrape_timeout__\"", token.STRING, 0)), + "SeparatorByte": reflect.ValueOf(model.SeparatorByte), + "SignatureForLabels": reflect.ValueOf(model.SignatureForLabels), + "SignatureWithoutLabels": reflect.ValueOf(model.SignatureWithoutLabels), + "TimeFromUnix": reflect.ValueOf(model.TimeFromUnix), + "TimeFromUnixNano": reflect.ValueOf(model.TimeFromUnixNano), + "TmpLabelPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"__tmp_\"", token.STRING, 0)), + "ToEscapingScheme": reflect.ValueOf(model.ToEscapingScheme), + "UTF8Validation": reflect.ValueOf(model.UTF8Validation), + "UnderscoreEscaping": reflect.ValueOf(model.UnderscoreEscaping), + "UnescapeName": reflect.ValueOf(model.UnescapeName), + "UnsetValidation": reflect.ValueOf(model.UnsetValidation), + "ValMatrix": reflect.ValueOf(model.ValMatrix), + "ValNone": reflect.ValueOf(model.ValNone), + "ValScalar": reflect.ValueOf(model.ValScalar), + "ValString": reflect.ValueOf(model.ValString), + "ValVector": reflect.ValueOf(model.ValVector), + "ValueEncodingEscaping": reflect.ValueOf(model.ValueEncodingEscaping), + "ZeroSample": reflect.ValueOf(&model.ZeroSample).Elem(), + "ZeroSamplePair": reflect.ValueOf(&model.ZeroSamplePair).Elem(), // type definitions "Alert": reflect.ValueOf((*model.Alert)(nil)), From 11e112dd2ab4285d83f7826b69b7c46c4f217487 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Fri, 19 Sep 2025 09:28:22 +0200 Subject: [PATCH 080/173] Simplify victoria metrics validator SLO plugin Signed-off-by: Xabier Larrakoetxea --- .../validate_victoria_metrics_v1/README.md | 14 +++++ .../validate_victoria_metrics_v1/plugin.go | 51 ++++++------------- 2 files changed, 29 insertions(+), 36 deletions(-) diff --git a/internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md index 2c8a4943..885af953 100644 --- a/internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md +++ b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md @@ -45,3 +45,17 @@ sloth generate \ -s '{"id": "sloth.dev/core/metadata_rules/v1"}' \ -s '{"id": "sloth.dev/core/alert_rules/v1"}' ``` + +### Explicit usage using env vars in script + +```bash +#! /bin/bash + +export SLOTH_DISABLE_DEFAULT_SLO_PLUGINS=true +export SLOTH_SLO_PLUGINS='{"id": "sloth.dev/contrib/validate_victoria_metrics/v1"} +{"id": "sloth.dev/core/sli_rules/v1"} +{"id": "sloth.dev/core/metadata_rules/v1"} +{"id": "sloth.dev/core/alert_rules/v1"}' + +sloth generate -i ./examples/victoria-metrics.yml +``` diff --git a/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go index 25dd6837..e55c44d8 100644 --- a/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go +++ b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go @@ -8,7 +8,6 @@ import ( "text/template" "github.com/VictoriaMetrics/metricsql" - prommodel "github.com/prometheus/common/model" "github.com/slok/sloth/pkg/common/validation" pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" @@ -39,51 +38,31 @@ func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, re } // VictoriaMetricsDialectValidator is the SLO flavour validator for victoria metrics backends dialect. -const VictoriaMetricsDialectValidator = victoriaMetricsDialectValidator(false) - -type victoriaMetricsDialectValidator bool - -func (victoriaMetricsDialectValidator) ValidateLabelKey(k string) error { - if k == prommodel.MetricNameLabel { - return fmt.Errorf("the label key %q is not allowed", prommodel.MetricNameLabel) - } - - if !prommodel.UTF8Validation.IsValidLabelName(k) { - return fmt.Errorf("the label key %q is not valid", k) - } - - return nil +var VictoriaMetricsDialectValidator = victoriaMetricsDialectValidator{ + promValidator: validation.PromQLDialectValidator, } -func (victoriaMetricsDialectValidator) ValidateLabelValue(k string) error { - if k == "" { - return fmt.Errorf("the label value is required") - } - - if !prommodel.LabelValue(k).IsValid() { - return fmt.Errorf("the label value %q is not valid", k) - } - - return nil +type victoriaMetricsDialectValidator struct { + promValidator validation.SLODialectValidator } -func (victoriaMetricsDialectValidator) ValidateAnnotationKey(k string) error { - if !prommodel.UTF8Validation.IsValidLabelName(k) { - return fmt.Errorf("the annotation key %q is not valid", k) - } +var promExprTplAllowedFakeData = map[string]string{"window": "1m"} - return nil +func (v victoriaMetricsDialectValidator) ValidateLabelKey(key string) error { + return v.promValidator.ValidateLabelKey(key) } -func (victoriaMetricsDialectValidator) ValidateAnnotationValue(k string) error { - if k == "" { - return fmt.Errorf("the annotation value is required") - } +func (v victoriaMetricsDialectValidator) ValidateLabelValue(label string) error { + return v.promValidator.ValidateLabelValue(label) +} - return nil +func (v victoriaMetricsDialectValidator) ValidateAnnotationKey(key string) error { + return v.promValidator.ValidateAnnotationKey(key) } -var promExprTplAllowedFakeData = map[string]string{"window": "1m"} +func (v victoriaMetricsDialectValidator) ValidateAnnotationValue(annot string) error { + return v.promValidator.ValidateAnnotationValue(annot) +} func (victoriaMetricsDialectValidator) ValidateQueryExpression(queryExpression string) error { if queryExpression == "" { From 13a044c2d48ecb525ba68347c552178309fb4ff6 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 20 Sep 2025 13:16:46 +0200 Subject: [PATCH 081/173] Increase integration tests in k8s eventual consistency waits Signed-off-by: Xabier Larrakoetxea --- .../k8scontroller/k8scontroller_test.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/integration/k8scontroller/k8scontroller_test.go b/test/integration/k8scontroller/k8scontroller_test.go index 59a338a2..0b6abcbf 100644 --- a/test/integration/k8scontroller/k8scontroller_test.go +++ b/test/integration/k8scontroller/k8scontroller_test.go @@ -17,6 +17,8 @@ import ( "github.com/slok/sloth/test/integration/testutils" ) +const controllerEventualConsistencySleep = 2000 * time.Millisecond + // sanitizePrometheusRule will remove all the dynamic fields on a monitoringv1.PrometheusRule object // these fileds are normally set by Kubernetes. func sanitizePrometheusRule(pr *monitoringv1.PrometheusRule) *monitoringv1.PrometheusRule { @@ -60,7 +62,7 @@ func TestKubernetesControllerPromOperatorGenerate(t *testing.T) { require.NoError(t, err) // Wait to be sure the controller had time for handling. - time.Sleep(500 * time.Millisecond) + time.Sleep(controllerEventualConsistencySleep) // Check. expRule := getBasePromOpPrometheusRule(version) @@ -83,7 +85,7 @@ func TestKubernetesControllerPromOperatorGenerate(t *testing.T) { require.NoError(t, err) // Wait to be sure the controller had time for handling. - time.Sleep(500 * time.Millisecond) + time.Sleep(controllerEventualConsistencySleep) // Check. expRule := getBase28DayPromOpPrometheusRule(version) @@ -106,7 +108,7 @@ func TestKubernetesControllerPromOperatorGenerate(t *testing.T) { require.NoError(t, err) // Wait to be sure the controller had time for handling. - time.Sleep(500 * time.Millisecond) + time.Sleep(controllerEventualConsistencySleep) // Check. expRule := getSLIPluginsPromOpPrometheusRule(version) @@ -129,7 +131,7 @@ func TestKubernetesControllerPromOperatorGenerate(t *testing.T) { require.NoError(t, err) // Wait to be sure the controller had time for handling. - time.Sleep(500 * time.Millisecond) + time.Sleep(controllerEventualConsistencySleep) // Check. expRule := getSLOPluginsPromOpPrometheusRule(version) @@ -152,7 +154,7 @@ func TestKubernetesControllerPromOperatorGenerate(t *testing.T) { require.NoError(t, err) // Wait to be sure the controller had time for handling. - time.Sleep(500 * time.Millisecond) + time.Sleep(controllerEventualConsistencySleep) // Check. gotSLOs, err := kubeClis.Sloth.SlothV1().PrometheusServiceLevels(ns).Get(ctx, SLOs.Name, metav1.GetOptions{}) @@ -180,7 +182,7 @@ func TestKubernetesControllerPromOperatorGenerate(t *testing.T) { require.NoError(t, err) // Wait to be sure the controller had time for handling. - time.Sleep(500 * time.Millisecond) + time.Sleep(controllerEventualConsistencySleep) // Check. gotSLOs, err := kubeClis.Sloth.SlothV1().PrometheusServiceLevels(ns).Get(ctx, SLOs.Name, metav1.GetOptions{}) @@ -208,7 +210,7 @@ func TestKubernetesControllerPromOperatorGenerate(t *testing.T) { require.NoError(t, err) // Wait to be sure the controller had time for handling. - time.Sleep(500 * time.Millisecond) + time.Sleep(controllerEventualConsistencySleep) // Check. expRule := getBase7DayPromOpPrometheusRule(version) From b6efc80e8c7460eabdfc2a218d5976f2ba7de967 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 20 Sep 2025 10:55:57 +0200 Subject: [PATCH 082/173] Add rule intervals SLO plugin Signed-off-by: Xabier Larrakoetxea --- .github/CODEOWNERS | 1 + CHANGELOG.md | 1 + examples/_gen/contrib-slo-plugins.yml | 246 ++++++++++++++++++ examples/contrib-slo-plugins.yml | 29 +++ .../slo/contrib/rule_intervals_v1/README.md | 46 ++++ .../slo/contrib/rule_intervals_v1/plugin.go | 63 +++++ .../contrib/rule_intervals_v1/plugin_test.go | 122 +++++++++ 7 files changed, 508 insertions(+) create mode 100644 internal/plugin/slo/contrib/rule_intervals_v1/README.md create mode 100644 internal/plugin/slo/contrib/rule_intervals_v1/plugin.go create mode 100644 internal/plugin/slo/contrib/rule_intervals_v1/plugin_test.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c277bc1d..2dcf570c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,3 +7,4 @@ # Specific contrib SLO owners. /internal/plugin/slo/contrib/info_labels_v1/ @cxdy @slok @wbollock /internal/plugin/slo/contrib/validate_victoria_metrics_v1/ @slok +/internal/plugin/slo/contrib/rule_intervals_v1/ @cxdy @slok @wbollock diff --git a/CHANGELOG.md b/CHANGELOG.md index cea800c0..79e340eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Contrib plugin: `/internal/plugin/slo/contrib/info_labels_v1/`. - Allow `github.com/VictoriaMetrics/metricsql` module in SLO plugins. - Contrib plugin: `sloth.dev/contrib/validate_victoria_metrics/v1`. +- Contrib plugin: `sloth.dev/contrib/rule_intervals/v1`. ## [v0.13.0] - 2025-09-10 diff --git a/examples/_gen/contrib-slo-plugins.yml b/examples/_gen/contrib-slo-plugins.yml index a83da44c..0d8e378b 100644 --- a/examples/_gen/contrib-slo-plugins.yml +++ b/examples/_gen/contrib-slo-plugins.yml @@ -250,3 +250,249 @@ groups: summary: High error rate on 'myservice' requests responses title: (ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget burn rate is too fast. +- name: sloth-slo-sli-recordings-myservice-requests-availability-contrib-plugin-rule-intervals-v1 + interval: 5m + rules: + - record: slo:sli_error:ratio_rate5m + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[5m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[5m]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + sloth_window: 5m + tier: "2" + - record: slo:sli_error:ratio_rate30m + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[30m]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[30m]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + sloth_window: 30m + tier: "2" + - record: slo:sli_error:ratio_rate1h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + sloth_window: 1h + tier: "2" + - record: slo:sli_error:ratio_rate2h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[2h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[2h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + sloth_window: 2h + tier: "2" + - record: slo:sli_error:ratio_rate6h + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[6h]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[6h]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + sloth_window: 6h + tier: "2" + - record: slo:sli_error:ratio_rate1d + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1d]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + sloth_window: 1d + tier: "2" + - record: slo:sli_error:ratio_rate3d + expr: | + (sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[3d]))) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[3d]))) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + sloth_window: 3d + tier: "2" + - record: slo:sli_error:ratio_rate30d + expr: | + sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"}[30d]) + / ignoring (sloth_window) + count_over_time(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"}[30d]) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + sloth_window: 30d + tier: "2" +- name: sloth-slo-meta-recordings-myservice-requests-availability-contrib-plugin-rule-intervals-v1 + interval: 5m + rules: + - record: slo:objective:ratio + expr: vector(0.9990000000000001) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + tier: "2" + - record: slo:error_budget:ratio + expr: vector(1-0.9990000000000001) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + tier: "2" + - record: slo:time_period:days + expr: vector(30) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + tier: "2" + - record: slo:current_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + tier: "2" + - record: slo:period_burn_rate:ratio + expr: | + slo:sli_error:ratio_rate30d{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + tier: "2" + - record: slo:period_error_budget_remaining:ratio + expr: 1 - slo:period_burn_rate:ratio{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", + sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + tier: "2" + - record: sloth_slo_info + expr: vector(1) + labels: + cmd: examplesgen.sh + owner: myteam + repo: myorg/myservice + sloth_id: myservice-requests-availability-contrib-plugin-rule-intervals-v1 + sloth_mode: cli-gen-prom + sloth_objective: "99.9" + sloth_service: myservice + sloth_slo: requests-availability-contrib-plugin-rule-intervals-v1 + sloth_spec: prometheus/v1 + sloth_version: dev + tier: "2" +- name: sloth-slo-alerts-myservice-requests-availability-contrib-plugin-rule-intervals-v1 + interval: 5m + rules: + - alert: MyServiceHighErrorRate + expr: | + ( + max(slo:sli_error:ratio_rate5m{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} > (14.4 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1h{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} > (14.4 * 0.0009999999999999432)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate30m{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} > (6 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate6h{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} > (6 * 0.0009999999999999432)) without (sloth_window) + ) + labels: + category: availability + routing_key: myteam + severity: pageteam + sloth_severity: page + annotations: + summary: High error rate on 'myservice' requests responses + title: (page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. + - alert: MyServiceHighErrorRate + expr: | + ( + max(slo:sli_error:ratio_rate2h{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} > (3 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1d{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} > (3 * 0.0009999999999999432)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate6h{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} > (1 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate3d{sloth_id="myservice-requests-availability-contrib-plugin-rule-intervals-v1", sloth_service="myservice", sloth_slo="requests-availability-contrib-plugin-rule-intervals-v1"} > (1 * 0.0009999999999999432)) without (sloth_window) + ) + labels: + category: availability + severity: slack + slack_channel: '#alerts-myteam' + sloth_severity: ticket + annotations: + summary: High error rate on 'myservice' requests responses + title: (ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. diff --git a/examples/contrib-slo-plugins.yml b/examples/contrib-slo-plugins.yml index 7f0f12e0..1313387c 100644 --- a/examples/contrib-slo-plugins.yml +++ b/examples/contrib-slo-plugins.yml @@ -36,3 +36,32 @@ slos: labels: severity: "slack" slack_channel: "#alerts-myteam" + + - name: "requests-availability-contrib-plugin-rule-intervals-v1" + objective: 99.9 + description: "Common SLO based on availability for HTTP request responses." + plugins: + chain: + - id: "sloth.dev/contrib/rule_intervals/v1" + config: + interval: + default: 5m + sli: + events: + error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) + total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) + alerting: + name: MyServiceHighErrorRate + labels: + category: "availability" + annotations: + # Overwrite default Sloth SLO alert summmary on ticket and page alerts. + summary: "High error rate on 'myservice' requests responses" + page_alert: + labels: + severity: pageteam + routing_key: myteam + ticket_alert: + labels: + severity: "slack" + slack_channel: "#alerts-myteam" diff --git a/internal/plugin/slo/contrib/rule_intervals_v1/README.md b/internal/plugin/slo/contrib/rule_intervals_v1/README.md new file mode 100644 index 00000000..cce07e18 --- /dev/null +++ b/internal/plugin/slo/contrib/rule_intervals_v1/README.md @@ -0,0 +1,46 @@ +# sloth.dev/contrib/rule_intervals/v1 + +This plugin sets Prom rule evaluation intervals to the Prometheus generated rules. The intervals can be different depending on the type of rules, SLI, metadata and alerts. A default interval can be set for all rules. + +## Config + +| Field | Type | Required | Default | Description | +|-------------------|--------------------------|----------|---------|-------------------------------------------------------------------| +| `interval.default` | Prom time duration string | Yes | — | Fallback interval to use if no other interval is set. | +| `interval.sliError`| Prom time duration string | No | — | Evaluation rule interval for generated SLI error rules. | +| `interval.metadata`| Prom time duration string | No | — | Evaluation rule interval for generated metadata rules. | +| `interval.alert` | Prom time duration string | No | — | Evaluation rule interval for generated alert rules. | + +## Env var + +None + +## Order requirement + +This plugin must be placed **after** all rule generation. + +## Usage examples + +### Specific settings in an SLO group + +```yaml +slo_plugins: + chain: + - id: sloth.dev/contrib/rule_intervals/v1 + config: + interval: + default: 1m + sliError: 42s + metadata: 55s + alert: 11s +``` + +### Add a custom default interval to all rules and SLOs + +By adding a default interval at app level, all generated rules by sloth will have that interval + +```bash +sloth generate \ + -i ./examples/getting-started.yml \ + -s '{"id": "sloth.dev/contrib/rule_intervals/v1","config":{"interval": {"default":"2m"}}}' +``` diff --git a/internal/plugin/slo/contrib/rule_intervals_v1/plugin.go b/internal/plugin/slo/contrib/rule_intervals_v1/plugin.go new file mode 100644 index 00000000..858500fc --- /dev/null +++ b/internal/plugin/slo/contrib/rule_intervals_v1/plugin.go @@ -0,0 +1,63 @@ +package plugin + +import ( + "context" + "encoding/json" + "fmt" + "time" + + prommodel "github.com/prometheus/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/contrib/rule_intervals/v1" +) + +type ConfigInterval struct { + Default prommodel.Duration `json:"default,omitempty"` + SLIError prommodel.Duration `json:"sliError,omitempty"` + Metadata prommodel.Duration `json:"metadata,omitempty"` + Alert prommodel.Duration `json:"alert,omitempty"` +} +type Config struct { + Interval ConfigInterval `json:"interval,omitempty"` +} + +func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + config := Config{} + err := json.Unmarshal(configData, &config) + if err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + if config.Interval.Default == 0 { + return nil, fmt.Errorf("at least default interval is required") + } + + return plugin{config: config}, nil +} + +type plugin struct { + config Config +} + +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + result.SLORules.SLIErrorRecRules.Interval = time.Duration(p.config.Interval.Default) + if p.config.Interval.SLIError != 0 { + result.SLORules.SLIErrorRecRules.Interval = time.Duration(p.config.Interval.SLIError) + } + + result.SLORules.MetadataRecRules.Interval = time.Duration(p.config.Interval.Default) + if p.config.Interval.Metadata != 0 { + result.SLORules.MetadataRecRules.Interval = time.Duration(p.config.Interval.Metadata) + } + + result.SLORules.AlertRules.Interval = time.Duration(p.config.Interval.Default) + if p.config.Interval.Alert != 0 { + result.SLORules.AlertRules.Interval = time.Duration(p.config.Interval.Alert) + } + + return nil +} diff --git a/internal/plugin/slo/contrib/rule_intervals_v1/plugin_test.go b/internal/plugin/slo/contrib/rule_intervals_v1/plugin_test.go new file mode 100644 index 00000000..cfe9c55f --- /dev/null +++ b/internal/plugin/slo/contrib/rule_intervals_v1/plugin_test.go @@ -0,0 +1,122 @@ +package plugin_test + +import ( + "encoding/json" + "testing" + "time" + + prommodel "github.com/prometheus/common/model" + "github.com/stretchr/testify/assert" + + plugin "github.com/slok/sloth/internal/plugin/slo/contrib/rule_intervals_v1" + "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" +) + +func MustJSONRawMessage(t *testing.T, v any) json.RawMessage { + j, err := json.Marshal(v) + if err != nil { + t.Fatalf("failed to marshal %T: %v", v, err) + } + return j +} + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + config json.RawMessage + req pluginslov1.Request + expRes pluginslov1.Result + expLoadErr bool + expErr bool + }{ + "A config without default time should fail.": { + config: MustJSONRawMessage(t, plugin.Config{Interval: plugin.ConfigInterval{ + SLIError: prommodel.Duration(43 * time.Second), + Metadata: prommodel.Duration(44 * time.Second), + Alert: prommodel.Duration(45 * time.Second), + }}), + expLoadErr: true, + }, + + "Having intervals for each of the rule types, it should load them.": { + config: MustJSONRawMessage(t, plugin.Config{Interval: plugin.ConfigInterval{ + Default: prommodel.Duration(42 * time.Second), + SLIError: prommodel.Duration(43 * time.Second), + Metadata: prommodel.Duration(44 * time.Second), + Alert: prommodel.Duration(45 * time.Second), + }}), + req: pluginslov1.Request{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Interval: 43 * time.Second}, + MetadataRecRules: model.PromRuleGroup{Interval: 44 * time.Second}, + AlertRules: model.PromRuleGroup{Interval: 45 * time.Second}, + }, + }, + }, + + "Having empty intervals for specific rule types, should fallback.": { + config: MustJSONRawMessage(t, plugin.Config{Interval: plugin.ConfigInterval{ + Default: prommodel.Duration(42 * time.Second), + }}), + req: pluginslov1.Request{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Interval: 42 * time.Second}, + MetadataRecRules: model.PromRuleGroup{Interval: 42 * time.Second}, + AlertRules: model.PromRuleGroup{Interval: 42 * time.Second}, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: test.config}) + if test.expLoadErr { + assert.Error(err) + return + } + assert.NoError(err) + + res := pluginslov1.Result{} + err = plugin.ProcessSLO(t.Context(), &test.req, &res) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, res) + } + }) + } +} + +func BenchmarkPluginYaegi(b *testing.B) { + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{}) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPluginGo(b *testing.B) { + plugin, err := plugin.NewPlugin(nil, pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} From b800230ffb09d438ca32d61b6a40dcbd37c5f284 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 07:01:45 +0000 Subject: [PATCH 083/173] build(deps): bump github.com/prometheus/prometheus Bumps [github.com/prometheus/prometheus](https://github.com/prometheus/prometheus) from 0.305.0 to 0.306.0. - [Release notes](https://github.com/prometheus/prometheus/releases) - [Changelog](https://github.com/prometheus/prometheus/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/prometheus/compare/v0.305.0...v0.306.0) --- updated-dependencies: - dependency-name: github.com/prometheus/prometheus dependency-version: 0.306.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 4e830f86..324b3a6d 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/prometheus-operator/prometheus-operator/pkg/client v0.85.1-0.20250905145128-3d04de8b9cba github.com/prometheus/client_golang v1.23.2 github.com/prometheus/common v0.66.1 - github.com/prometheus/prometheus v0.305.0 + github.com/prometheus/prometheus v0.306.0 github.com/sirupsen/logrus v1.9.3 github.com/slok/reload v0.2.0 github.com/spotahome/kooper/v2 v2.9.0 diff --git a/go.sum b/go.sum index dc4646ff..4160c6a2 100644 --- a/go.sum +++ b/go.sum @@ -184,8 +184,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= -github.com/prometheus/prometheus v0.305.0 h1:UO/LsM32/E9yBDtvQj8tN+WwhbyWKR10lO35vmFLx0U= -github.com/prometheus/prometheus v0.305.0/go.mod h1:JG+jKIDUJ9Bn97anZiCjwCxRyAx+lpcEQ0QnZlUlbwY= +github.com/prometheus/prometheus v0.306.0 h1:Q0Pvz/ZKS6vVWCa1VSgNyNJlEe8hxdRlKklFg7SRhNw= +github.com/prometheus/prometheus v0.306.0/go.mod h1:7hMSGyZHt0dcmZ5r4kFPJ/vxPQU99N5/BGwSPDxeZrQ= github.com/prometheus/sigv4 v0.2.0 h1:qDFKnHYFswJxdzGeRP63c4HlH3Vbn1Yf/Ao2zabtVXk= github.com/prometheus/sigv4 v0.2.0/go.mod h1:D04rqmAaPPEUkjRQxGqjoxdyJuyCh6E0M18fZr0zBiE= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= @@ -287,8 +287,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.238.0 h1:+EldkglWIg/pWjkq97sd+XxH7PxakNYoe/rkSTbnvOs= -google.golang.org/api v0.238.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo= +google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= From c47c6c18002426e751a0d3f369d46dbb61c3e771 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Mon, 22 Sep 2025 12:08:54 -0400 Subject: [PATCH 084/173] Merge pull request #670 from wbollock/feat/error_budget_exhausted_plugin feat: error budget exhausted plugin --- .github/CODEOWNERS | 7 +- .gitignore | 3 + .../error_budget_exhausted_alert_v1/README.md | 51 +++ .../error_budget_exhausted_alert_v1/plugin.go | 101 +++++ .../plugin_test.go | 429 ++++++++++++++++++ 5 files changed, 588 insertions(+), 3 deletions(-) create mode 100644 internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/README.md create mode 100644 internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin.go create mode 100644 internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin_test.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2dcf570c..41507386 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,7 @@ /internal/plugin/slo/contrib/ @slok # Specific contrib SLO owners. -/internal/plugin/slo/contrib/info_labels_v1/ @cxdy @slok @wbollock -/internal/plugin/slo/contrib/validate_victoria_metrics_v1/ @slok -/internal/plugin/slo/contrib/rule_intervals_v1/ @cxdy @slok @wbollock +/internal/plugin/slo/contrib/info_labels_v1/ @cxdy @slok @wbollock +/internal/plugin/slo/contrib/validate_victoria_metrics_v1/ @slok +/internal/plugin/slo/contrib/rule_intervals_v1/ @cxdy @slok @wbollock +/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/ @cxdy @slok @wbollock diff --git a/.gitignore b/.gitignore index bf5602b2..9ec36ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ vendor/ # Binaries /bin + +# mise/asdf +.tool-versions diff --git a/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/README.md b/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/README.md new file mode 100644 index 00000000..7028a4ef --- /dev/null +++ b/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/README.md @@ -0,0 +1,51 @@ +# sloth.dev/contrib/error_budget_exhausted_alert/v1 + +This plugin creates an additional alert when an error budget is totally depleted. It is useful to initiate organization policies around error budget depletion, like a change freeze or retrospective. It is more informational than directly actionable as other burn alerts should fire first but the threshold is customizable for a variety of scenarios. + +## Config + +| Field | Type | Required | Default | Description | +| ----------------- | ----------------- | -------- | ------------------------ | ----------------------------------------------------------- | +| `threshold` | float64 | No | `0.0` | Error budget remaining threshold | +| `for` | string | No | `"5m"` | Duration before firing alert | +| `annotations` | map[string]string | No | `{}` | Alert annotations | +| `alert_name` | string | No | `"ErrorBudgetExhausted"` | Alert rule name | +| `alert_labels` | map[string]string | No | `{}` | Additional labels on the alert | +| `selector_labels` | map[string]string | No | `{}` | Additional selector labels on the time series for the alert | + +## Env vars + +None. + +## Order requirement + +This plugin should run after metadata rules generation plugins. + +## Usage examples + +### Basic Usage + +```yaml +chain: + - id: "sloth.dev/contrib/error_budget_exhausted_alert/v1" + config: + alert_labels: + severity: "info" +``` + +### Custom Configuration + +```yaml +chain: + - id: "sloth.dev/contrib/error_budget_exhausted_alert/v1" + config: + threshold: 0.05 + alert_name: "ErrorBudgetLow" + alert_labels: + severity: "info" + environmnet: "production" + selector_labels: + datacenter: "us-east" + annotations: + description: "Error budget low for SLO {{ $labels.sloth_slo }}" +``` diff --git a/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin.go b/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin.go new file mode 100644 index 00000000..2e254d18 --- /dev/null +++ b/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin.go @@ -0,0 +1,101 @@ +package plugin + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/rulefmt" + + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/contrib/error_budget_exhausted_alert/v1" +) + +type Config struct { + Threshold float64 `json:"threshold"` // default 0, fully exhausted + For model.Duration `json:"for"` // default 5m + Annotations map[string]string `json:"annotations,omitempty"` // default empty, additional annotations to add to the alert + AlertName string `json:"alert_name,omitempty"` // default "ErrorBudgetExhausted" + SelectorLabels map[string]string `json:"selector_labels,omitempty"` // default empty, additional labels to determine what should alert + AlertLabels map[string]string `json:"alert_labels,omitempty"` // default empty, additional labels to add to the alert +} + +func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + cfg := Config{ + Threshold: 0, + AlertLabels: map[string]string{}, + Annotations: map[string]string{}, + SelectorLabels: map[string]string{}, + } + if err := json.Unmarshal(configData, &cfg); err != nil { + return nil, fmt.Errorf("invalid plugin config: %w", err) + } + + if cfg.For == 0 { + cfg.For = model.Duration(5 * time.Minute) + } + + if cfg.AlertName == "" { + cfg.AlertName = "ErrorBudgetExhausted" + } + + return plugin{config: cfg}, nil +} + +type plugin struct { + config Config +} + +// labelMatcher takes a map of labels and returns a string for PromQL inclusion. +func labelMatcher(labels map[string]string) string { + ls := model.LabelSet{} + for k, v := range labels { + ls[model.LabelName(k)] = model.LabelValue(v) + } + return ls.String() +} + +func (p plugin) ProcessSLO(_ context.Context, req *pluginslov1.Request, result *pluginslov1.Result) error { + slo := &req.SLO + + // Base labels for the alert + labels := map[string]string{ + "sloth_slo": slo.Name, + "sloth_service": slo.Service, + "sloth_id": fmt.Sprintf("%s-%s", slo.Service, slo.Name), + } + + // Add all SLO custom labels + for k, v := range slo.Labels { + labels[k] = v + } + + // Add any selector labels from config + for k, v := range p.config.SelectorLabels { + labels[k] = v + } + + expr := fmt.Sprintf(`slo:period_error_budget_remaining:ratio%s <= %g`, labelMatcher(labels), p.config.Threshold) + + // Alert annotations mixed in too + annotations := make(map[string]string) + for k, v := range p.config.Annotations { + annotations[k] = v + } + + result.SLORules.AlertRules.Rules = append(result.SLORules.AlertRules.Rules, rulefmt.Rule{ + Alert: p.config.AlertName, + Expr: expr, + For: p.config.For, + Labels: p.config.AlertLabels, + Annotations: annotations, + }) + + return nil +} diff --git a/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin_test.go b/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin_test.go new file mode 100644 index 00000000..c7299852 --- /dev/null +++ b/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin_test.go @@ -0,0 +1,429 @@ +package plugin_test + +import ( + "encoding/json" + "testing" + "time" + + prommodel "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/stretchr/testify/assert" + + plugin "github.com/slok/sloth/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1" + "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" +) + +func MustJSONRawMessage(t *testing.T, v any) json.RawMessage { + j, err := json.Marshal(v) + if err != nil { + t.Fatalf("failed to marshal %T: %v", v, err) + } + return j +} + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + config json.RawMessage + req pluginslov1.Request + res pluginslov1.Result + expRes pluginslov1.Result + expLoadErr bool + expErr bool + }{ + "A config without for duration should *not* fail.": { + config: MustJSONRawMessage(t, plugin.Config{ + Threshold: 0.0, + AlertName: "TestAlert", + }), + req: pluginslov1.Request{ + SLO: model.PromSLO{ + Name: "availability", + Service: "web-service", + }, + }, + res: pluginslov1.Result{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + AlertRules: model.PromRuleGroup{ + Rules: []rulefmt.Rule{ + { + Alert: "TestAlert", + Expr: `slo:period_error_budget_remaining:ratio{sloth_id="web-service-availability", sloth_service="web-service", sloth_slo="availability"} <= 0`, + For: prommodel.Duration(5 * time.Minute), + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + }, + }, + }, + }, + expLoadErr: false, + }, + + "SLO with custom labels should include them in expression.": { + config: MustJSONRawMessage(t, plugin.Config{ + Threshold: 0.0, + AlertName: "TestAlert", + }), + req: pluginslov1.Request{ + SLO: model.PromSLO{ + Name: "availability", + Service: "web-service", + Labels: map[string]string{ + "environment": "production", + "team": "platform", + }, + }, + }, + res: pluginslov1.Result{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + AlertRules: model.PromRuleGroup{ + Rules: []rulefmt.Rule{ + { + Alert: "TestAlert", + Expr: `slo:period_error_budget_remaining:ratio{environment="production", sloth_id="web-service-availability", sloth_service="web-service", sloth_slo="availability", team="platform"} <= 0`, + For: prommodel.Duration(5 * time.Minute), + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + }, + }, + }, + }, + expLoadErr: false, + }, + + "Config with selector labels should include them in expression.": { + config: MustJSONRawMessage(t, plugin.Config{ + Threshold: 0.0, + AlertName: "TestAlert", + SelectorLabels: map[string]string{ + "region": "us-west-2", + "tier": "critical", + }, + }), + req: pluginslov1.Request{ + SLO: model.PromSLO{ + Name: "availability", + Service: "web-service", + }, + }, + res: pluginslov1.Result{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + AlertRules: model.PromRuleGroup{ + Rules: []rulefmt.Rule{ + { + Alert: "TestAlert", + Expr: `slo:period_error_budget_remaining:ratio{region="us-west-2", sloth_id="web-service-availability", sloth_service="web-service", sloth_slo="availability", tier="critical"} <= 0`, + For: prommodel.Duration(5 * time.Minute), + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + }, + }, + }, + }, + expLoadErr: false, + }, + + "A config with invalid duration format should fail.": { + config: json.RawMessage(`{"threshold": 0.0, "for": "invalid-duration", "alert_name": "TestAlert"}`), + expLoadErr: true, + }, + + "A config with negative threshold should *not* fail.": { + config: MustJSONRawMessage(t, plugin.Config{ + Threshold: -0.1, + For: prommodel.Duration(5 * time.Minute), + AlertName: "TestAlert", + }), + req: pluginslov1.Request{ + SLO: model.PromSLO{ + Name: "error-rate", + Service: "payment-service", + }, + }, + res: pluginslov1.Result{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + AlertRules: model.PromRuleGroup{ + Rules: []rulefmt.Rule{ + { + Alert: "TestAlert", + Expr: `slo:period_error_budget_remaining:ratio{sloth_id="payment-service-error-rate", sloth_service="payment-service", sloth_slo="error-rate"} <= -0.1`, + For: prommodel.Duration(5 * time.Minute), + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + }, + }, + }, + }, + expLoadErr: false, + }, + + "Creating alert rule for exhausted error budget should generate correct rule.": { + config: MustJSONRawMessage(t, plugin.Config{ + Threshold: 0.0, + For: prommodel.Duration(5 * time.Minute), + AlertName: "ErrorBudgetExhausted", + AlertLabels: map[string]string{ + "severity": "critical", + "team": "platform", + }, + }), + req: pluginslov1.Request{ + SLO: model.PromSLO{ + Name: "availability", + Service: "checkout", + }, + }, + res: pluginslov1.Result{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + AlertRules: model.PromRuleGroup{ + Rules: []rulefmt.Rule{ + { + Alert: "ErrorBudgetExhausted", + Expr: `slo:period_error_budget_remaining:ratio{sloth_id="checkout-availability", sloth_service="checkout", sloth_slo="availability"} <= 0`, + For: prommodel.Duration(5 * time.Minute), + Labels: map[string]string{ + "severity": "critical", + "team": "platform", + }, + Annotations: map[string]string{}, + }, + }, + }, + }, + }, + }, + + "Creating alert rule with positive threshold should generate correct promql expression.": { + config: MustJSONRawMessage(t, plugin.Config{ + Threshold: 0.05, + For: prommodel.Duration(10 * time.Minute), + AlertName: "ErrorBudgetLow", + AlertLabels: map[string]string{ + "severity": "warning", + }, + }), + req: pluginslov1.Request{ + SLO: model.PromSLO{ + Name: "latency", + Service: "api-gateway", + }, + }, + res: pluginslov1.Result{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + AlertRules: model.PromRuleGroup{ + Rules: []rulefmt.Rule{ + { + Alert: "ErrorBudgetLow", + Expr: `slo:period_error_budget_remaining:ratio{sloth_id="api-gateway-latency", sloth_service="api-gateway", sloth_slo="latency"} <= 0.05`, + For: prommodel.Duration(10 * time.Minute), + Labels: map[string]string{ + "severity": "warning", + }, + Annotations: map[string]string{}, + }, + }, + }, + }, + }, + }, + + "A config without alert name should not fail and use default name.": { + config: MustJSONRawMessage(t, map[string]interface{}{ + "threshold": 0.0, + "for": "5m", + }), + req: pluginslov1.Request{ + SLO: model.PromSLO{ + Name: "reliability", + Service: "auth-service", + }, + }, + res: pluginslov1.Result{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + AlertRules: model.PromRuleGroup{ + Rules: []rulefmt.Rule{ + { + Alert: "ErrorBudgetExhausted", + Expr: `slo:period_error_budget_remaining:ratio{sloth_id="auth-service-reliability", sloth_service="auth-service", sloth_slo="reliability"} <= 0`, + For: prommodel.Duration(5 * time.Minute), + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + }, + }, + }, + }, + expLoadErr: false, + }, + + "Config with both selector and alert labels should separate them correctly.": { + config: MustJSONRawMessage(t, plugin.Config{ + Threshold: 0.0, + AlertName: "TestAlert", + SelectorLabels: map[string]string{ + "region": "us-west-2", + }, + AlertLabels: map[string]string{ + "severity": "critical", + }, + }), + req: pluginslov1.Request{ + SLO: model.PromSLO{ + Name: "availability", + Service: "web-service", + }, + }, + res: pluginslov1.Result{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + AlertRules: model.PromRuleGroup{ + Rules: []rulefmt.Rule{ + { + Alert: "TestAlert", + Expr: `slo:period_error_budget_remaining:ratio{region="us-west-2", sloth_id="web-service-availability", sloth_service="web-service", sloth_slo="availability"} <= 0`, + For: prommodel.Duration(5 * time.Minute), + Labels: map[string]string{ + "severity": "critical", + }, + Annotations: map[string]string{}, + }, + }, + }, + }, + }, + }, + + "Config with custom annotations should include them in alert rule.": { + config: MustJSONRawMessage(t, plugin.Config{ + Threshold: 0.0, + AlertName: "TestAlert", + Annotations: map[string]string{ + "description": "Custom error budget alert for {{ $labels.sloth_slo }}", + "runbook_url": "https://example.com/runbook", + "summary": "Error budget exhausted", + }, + }), + req: pluginslov1.Request{ + SLO: model.PromSLO{ + Name: "availability", + Service: "web-service", + }, + }, + res: pluginslov1.Result{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + AlertRules: model.PromRuleGroup{ + Rules: []rulefmt.Rule{ + { + Alert: "TestAlert", + Expr: `slo:period_error_budget_remaining:ratio{sloth_id="web-service-availability", sloth_service="web-service", sloth_slo="availability"} <= 0`, + For: prommodel.Duration(5 * time.Minute), + Labels: map[string]string{}, + Annotations: map[string]string{ + "description": "Custom error budget alert for {{ $labels.sloth_slo }}", + "runbook_url": "https://example.com/runbook", + "summary": "Error budget exhausted", + }, + }, + }, + }, + }, + }, + expLoadErr: false, + }, + + "Creating alert rule with minimal config should work without annotations.": { + config: MustJSONRawMessage(t, plugin.Config{ + Threshold: 0.1, + For: prommodel.Duration(15 * time.Minute), + AlertName: "MinimalAlert", + }), + req: pluginslov1.Request{ + SLO: model.PromSLO{ + Name: "uptime", + Service: "database", + }, + }, + res: pluginslov1.Result{}, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + AlertRules: model.PromRuleGroup{ + Rules: []rulefmt.Rule{ + { + Alert: "MinimalAlert", + Expr: `slo:period_error_budget_remaining:ratio{sloth_id="database-uptime", sloth_service="database", sloth_slo="uptime"} <= 0.1`, + For: prommodel.Duration(15 * time.Minute), + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + }, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{ + PluginConfiguration: test.config, + }) + if test.expLoadErr { + assert.Error(err) + return + } + assert.NoError(err) + + err = plugin.ProcessSLO(t.Context(), &test.req, &test.res) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, test.res) + } + }) + } +} + +func BenchmarkPluginYaegi(b *testing.B) { + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{ + PluginConfiguration: []byte(`{"threshold":0.0,"for":"5m","alert_name":"ErrorBudgetExhausted"}`), + }) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPluginGo(b *testing.B) { + plugin, err := plugin.NewPlugin([]byte(`{"threshold":0.0,"for":"5m","alert_name":"ErrorBudgetExhausted"}`), pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} From 56a46d82b330543d843ffec2c2cdf9aa4ffd7a22 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Mon, 22 Sep 2025 20:39:58 +0200 Subject: [PATCH 085/173] Port denominator corrected feature as SLO plugin Signed-off-by: Xabier Larrakoetxea --- .github/CODEOWNERS | 9 +- CHANGELOG.md | 2 + .../_gen/contrib-denominator-corrected.yaml | 349 ++++++++++++++++ examples/contrib-denominator-corrected.yaml | 34 ++ .../denominator_corrected_rules_v1/README.md | 40 ++ .../denominator_corrected_rules_v1/plugin.go | 188 +++++++++ .../plugin_test.go | 385 ++++++++++++++++++ 7 files changed, 1003 insertions(+), 4 deletions(-) create mode 100644 examples/_gen/contrib-denominator-corrected.yaml create mode 100644 examples/contrib-denominator-corrected.yaml create mode 100644 internal/plugin/slo/contrib/denominator_corrected_rules_v1/README.md create mode 100644 internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go create mode 100644 internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin_test.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 41507386..821416e1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,7 +5,8 @@ /internal/plugin/slo/contrib/ @slok # Specific contrib SLO owners. -/internal/plugin/slo/contrib/info_labels_v1/ @cxdy @slok @wbollock -/internal/plugin/slo/contrib/validate_victoria_metrics_v1/ @slok -/internal/plugin/slo/contrib/rule_intervals_v1/ @cxdy @slok @wbollock -/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/ @cxdy @slok @wbollock +/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/ @cxdy @slok @wbollock +/internal/plugin/slo/contrib/denominator_corrected_rules/v1/ @slok +/internal/plugin/slo/contrib/info_labels_v1/ @cxdy @slok @wbollock +/internal/plugin/slo/contrib/rule_intervals_v1/ @cxdy @slok @wbollock +/internal/plugin/slo/contrib/validate_victoria_metrics_v1/ @slok diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e340eb..b23430bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - Allow `github.com/VictoriaMetrics/metricsql` module in SLO plugins. - Contrib plugin: `sloth.dev/contrib/validate_victoria_metrics/v1`. - Contrib plugin: `sloth.dev/contrib/rule_intervals/v1`. +- Contrib plugin: `sloth.dev/contrib/error_budget_exhausted_alert/v1`. +- Contrib plugin: `sloth.dev/contrib/denominator_corrected_rules/v1`. ## [v0.13.0] - 2025-09-10 diff --git a/examples/_gen/contrib-denominator-corrected.yaml b/examples/_gen/contrib-denominator-corrected.yaml new file mode 100644 index 00000000..d5864244 --- /dev/null +++ b/examples/_gen/contrib-denominator-corrected.yaml @@ -0,0 +1,349 @@ + +--- +# Code generated by Sloth (dev): https://github.com/slok/sloth. +# DO NOT EDIT. + +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + labels: + app.kubernetes.io/component: SLO + app.kubernetes.io/managed-by: sloth + name: svc + namespace: test-ns +spec: + groups: + - name: sloth-slo-sli-recordings-svc01-slo1 + rules: + - expr: | + ( + slo:numerator_correction:ratio5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + * on() + sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[5m])) + ) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[5m]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 5m + record: slo:sli_error:ratio_rate5m + - expr: | + ( + slo:numerator_correction:ratio30m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + * on() + sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[30m])) + ) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[30m]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 30m + record: slo:sli_error:ratio_rate30m + - expr: | + ( + slo:numerator_correction:ratio1h{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + * on() + sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1h])) + ) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1h]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 1h + record: slo:sli_error:ratio_rate1h + - expr: | + ( + slo:numerator_correction:ratio2h{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + * on() + sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[2h])) + ) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[2h]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 2h + record: slo:sli_error:ratio_rate2h + - expr: | + ( + slo:numerator_correction:ratio6h{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + * on() + sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[6h])) + ) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[6h]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 6h + record: slo:sli_error:ratio_rate6h + - expr: | + ( + slo:numerator_correction:ratio1d{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + * on() + sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[1d])) + ) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[1d]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 1d + record: slo:sli_error:ratio_rate1d + - expr: | + ( + slo:numerator_correction:ratio3d{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + * on() + sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[3d])) + ) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[3d]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 3d + record: slo:sli_error:ratio_rate3d + - expr: | + ( + slo:numerator_correction:ratio30d{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + * on() + sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[30d])) + ) + / + (sum(rate(http_request_duration_seconds_count{job="myservice"}[30d]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + sloth_window: 30d + record: slo:sli_error:ratio_rate30d + - name: sloth-slo-meta-recordings-svc01-slo1 + rules: + - expr: vector(0.9990000000000001) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:objective:ratio + - expr: vector(1-0.9990000000000001) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:error_budget:ratio + - expr: vector(30) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:time_period:days + - expr: | + slo:sli_error:ratio_rate5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:current_burn_rate:ratio + - expr: | + slo:sli_error:ratio_rate30d{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + / on(sloth_id, sloth_slo, sloth_service) group_left + slo:error_budget:ratio{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:period_burn_rate:ratio + - expr: 1 - slo:period_burn_rate:ratio{sloth_id="svc01-slo1", sloth_service="svc01", + sloth_slo="slo1"} + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:period_error_budget_remaining:ratio + - expr: vector(1) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_mode: cli-gen-k8s + sloth_objective: "99.9" + sloth_service: svc01 + sloth_slo: slo1 + sloth_spec: sloth.slok.dev/v1 + sloth_version: dev + record: sloth_slo_info + - expr: (sum(rate(http_request_duration_seconds_count{job="myservice"}[5m])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:numerator_correction:ratio5m + - expr: (sum(rate(http_request_duration_seconds_count{job="myservice"}[30m])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:numerator_correction:ratio30m + - expr: (sum(rate(http_request_duration_seconds_count{job="myservice"}[1h])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:numerator_correction:ratio1h + - expr: (sum(rate(http_request_duration_seconds_count{job="myservice"}[2h])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:numerator_correction:ratio2h + - expr: (sum(rate(http_request_duration_seconds_count{job="myservice"}[6h])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:numerator_correction:ratio6h + - expr: (sum(rate(http_request_duration_seconds_count{job="myservice"}[1d])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:numerator_correction:ratio1d + - expr: (sum(rate(http_request_duration_seconds_count{job="myservice"}[3d])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:numerator_correction:ratio3d + - expr: (sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d]))) + labels: + cmd: examplesgen.sh + global01k1: global01v1 + global02k1: global02v1 + sloth_id: svc01-slo1 + sloth_service: svc01 + sloth_slo: slo1 + record: slo:numerator_correction:ratio30d + - name: sloth-slo-alerts-svc01-slo1 + rules: + - alert: myServiceAlert + annotations: + alert02k1: alert02k2 + summary: '{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is over expected.' + title: (page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is too fast. + expr: | + ( + max(slo:sli_error:ratio_rate5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (14.4 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1h{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (14.4 * 0.0009999999999999432)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate30m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (6 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate6h{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (6 * 0.0009999999999999432)) without (sloth_window) + ) + labels: + alert01k1: alert01v1 + alert03k1: alert03v1 + sloth_severity: page + - alert: myServiceAlert + annotations: + alert02k1: alert02k2 + summary: '{{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget + burn rate is over expected.' + title: (ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error + budget burn rate is too fast. + expr: | + ( + max(slo:sli_error:ratio_rate2h{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (3 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate1d{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (3 * 0.0009999999999999432)) without (sloth_window) + ) + or + ( + max(slo:sli_error:ratio_rate6h{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (1 * 0.0009999999999999432)) without (sloth_window) + and + max(slo:sli_error:ratio_rate3d{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} > (1 * 0.0009999999999999432)) without (sloth_window) + ) + labels: + alert01k1: alert01v1 + alert04k1: alert04v1 + sloth_severity: ticket diff --git a/examples/contrib-denominator-corrected.yaml b/examples/contrib-denominator-corrected.yaml new file mode 100644 index 00000000..ca22da2f --- /dev/null +++ b/examples/contrib-denominator-corrected.yaml @@ -0,0 +1,34 @@ +apiVersion: sloth.slok.dev/v1 +kind: PrometheusServiceLevel +metadata: + name: svc + namespace: test-ns +spec: + service: "svc01" + labels: + global01k1: global01v1 + sloPlugins: + chain: + - id: "sloth.dev/contrib/denominator_corrected_rules/v1" + slos: + - name: "slo1" + objective: 99.9 + description: "This is SLO 01." + labels: + global02k1: global02v1 + sli: + events: + errorQuery: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) + totalQuery: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) + alerting: + name: myServiceAlert + labels: + alert01k1: "alert01v1" + annotations: + alert02k1: "alert02k2" + pageAlert: + labels: + alert03k1: "alert03v1" + ticketAlert: + labels: + alert04k1: "alert04v1" diff --git a/internal/plugin/slo/contrib/denominator_corrected_rules_v1/README.md b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/README.md new file mode 100644 index 00000000..9ed42521 --- /dev/null +++ b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/README.md @@ -0,0 +1,40 @@ +# sloth.dev/contrib/denominator_corrected_rules/v1 + +## High level explanation + +Plugin ported from [#459 Sloth PR][PR]. Full details are in the PR. + +**Note:** This plugin replaces all SLI recording rules and adds new metadata rules. + +This plugin adjusts SLOs for services with seasonal traffic patterns (for example: high traffic during the day, very low traffic at night). +Normally, SLOs treat all burn rates the same. But during low-traffic periods, even a few failed requests can cause false alerts and pages. + +To fix this, the plugin applies a correction factor based on request volume. The burn rate impact scales with traffic levels, higher traffic means failures weigh more, and lower traffic means they weigh less. + +If your service experiences low request volumes at certain times and you see noisy alerts, this plugin can help. + +More details in the original [PR]. + +## Config + +None + +## Env vars + +None + +## Order requirement + +This plugin should run after rule generation plugins. + +## Usage examples + +### Regular usage + +```yaml + sloPlugins: + chain: + - id: "sloth.dev/contrib/denominator_corrected_rules/v1" +``` + +[PR]: https://github.com/slok/sloth/pull/459 \ No newline at end of file diff --git a/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go new file mode 100644 index 00000000..bd1ea4f2 --- /dev/null +++ b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go @@ -0,0 +1,188 @@ +package plugin + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "sort" + "text/template" + "time" + + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/slok/sloth/pkg/common/conventions" + "github.com/slok/sloth/pkg/common/model" + utilsdata "github.com/slok/sloth/pkg/common/utils/data" + promutils "github.com/slok/sloth/pkg/common/utils/prometheus" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/contrib/denominator_corrected_rules/v1" +) + +type PluginConfig struct{} + +func NewPlugin(c json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + cfg := &PluginConfig{} + err := json.Unmarshal(c, cfg) + if err != nil { + return nil, err + } + + return plugin{cfg: *cfg}, nil +} + +type plugin struct { + cfg PluginConfig +} + +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + // Check requirements for this type of SLOs. + if request.SLO.SLI.Events == nil || request.SLO.SLI.Events.ErrorQuery == "" || request.SLO.SLI.Events.TotalQuery == "" { + return fmt.Errorf("denominator corrected SLI requires SLI event type") + } + + // Generate and override SLI recordings. + sliRules, err := generateSLIRecordingRules(ctx, request.SLO, request.MWMBAlertGroup) + if err != nil { + return err + } + result.SLORules.SLIErrorRecRules.Rules = sliRules + + // Add required new metadata recordings with the correction factor. + windows := getAlertGroupWindows(request.MWMBAlertGroup) + windows = append(windows, request.SLO.TimeWindow) // Add the total time window as a handy helper. + metadataLabels := utilsdata.MergeLabels(conventions.GetSLOIDPromLabels(request.SLO), request.SLO.Labels) + for _, window := range windows { + rule, err := createNumeratorCorrection(request.SLO, metadataLabels, window, request.SLO.TimeWindow) + if err != nil { + return fmt.Errorf("could not create numerator rule: %v", err) + } + result.SLORules.MetadataRecRules.Rules = append(result.SLORules.MetadataRecRules.Rules, *rule) + } + + return nil +} + +func generateSLIRecordingRules(ctx context.Context, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { + // Get the windows we need the recording rules. + windows := getAlertGroupWindows(alerts) + windows = append(windows, slo.TimeWindow) // Add the total time window as a handy helper. + + // Generate the rules + rules := make([]rulefmt.Rule, 0, len(windows)) + for _, window := range windows { + rule, err := denominatorCorrectedSLIRecordGenerator(slo, window, alerts) + if err != nil { + return nil, fmt.Errorf("could not create %q SLO rule for window %s: %w", slo.ID, window, err) + } + rules = append(rules, *rule) + } + + return rules, nil +} + +// getAlertGroupWindows gets all the time windows from a multiwindow multiburn alert group. +func getAlertGroupWindows(alerts model.MWMBAlertGroup) []time.Duration { + // Use a map to avoid duplicated windows. + windows := map[string]time.Duration{ + alerts.PageQuick.ShortWindow.String(): alerts.PageQuick.ShortWindow, + alerts.PageQuick.LongWindow.String(): alerts.PageQuick.LongWindow, + alerts.PageSlow.ShortWindow.String(): alerts.PageSlow.ShortWindow, + alerts.PageSlow.LongWindow.String(): alerts.PageSlow.LongWindow, + alerts.TicketQuick.ShortWindow.String(): alerts.TicketQuick.ShortWindow, + alerts.TicketQuick.LongWindow.String(): alerts.TicketQuick.LongWindow, + alerts.TicketSlow.ShortWindow.String(): alerts.TicketSlow.ShortWindow, + alerts.TicketSlow.LongWindow.String(): alerts.TicketSlow.LongWindow, + } + + res := make([]time.Duration, 0, len(windows)) + for _, w := range windows { + res = append(res, w) + } + sort.SliceStable(res, func(i, j int) bool { return res[i] < res[j] }) + + return res +} + +func denominatorCorrectedSLIRecordGenerator(slo model.PromSLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { + const sliExprTplFmt = `( +slo:numerator_correction:ratio{{.window}}{{.filter}} +* on() +%s +) +/ +(%s) +` + + sliExprTpl := fmt.Sprintf(sliExprTplFmt, slo.SLI.Events.ErrorQuery, slo.SLI.Events.TotalQuery) + + // Render with our templated data. + tpl, err := template.New("sliExpr").Option("missingkey=error").Parse(sliExprTpl) + if err != nil { + return nil, fmt.Errorf("could not create SLI expression template data: %w", err) + } + + strWindow := promutils.TimeDurationToPromStr(window) + + var b bytes.Buffer + err = tpl.Execute(&b, map[string]string{ + tplKeyWindow: strWindow, + "filter": promutils.LabelsToPromFilter(conventions.GetSLOIDPromLabels(slo)), + "windowKey": conventions.PromSLOWindowLabelName, + }) + if err != nil { + return nil, fmt.Errorf("could not render SLI expression template: %w", err) + } + + return &rulefmt.Rule{ + Record: conventions.GetSLIErrorMetric(window), + Expr: b.String(), + Labels: utilsdata.MergeLabels( + conventions.GetSLOIDPromLabels(slo), + map[string]string{ + conventions.PromSLOWindowLabelName: strWindow, + }, + slo.Labels, + ), + }, nil +} + +const ( + tplKeyWindow = "window" +) + +func createNumeratorCorrection(slo model.PromSLO, labels map[string]string, currentWindow, totalWindow time.Duration) (*rulefmt.Rule, error) { + windowString := promutils.TimeDurationToPromStr(currentWindow) + metricSLONumeratorCorrection := fmt.Sprintf("slo:numerator_correction:ratio%s", windowString) + + tpl, err := template.New("sliExpr").Option("missingkey=error").Parse(slo.SLI.Events.TotalQuery) + if err != nil { + return nil, fmt.Errorf("could not create %s expression template data: %w", metricSLONumeratorCorrection, err) + } + + var numeratorBuffer bytes.Buffer + err = tpl.Execute(&numeratorBuffer, map[string]string{ + tplKeyWindow: windowString, + }) + if err != nil { + return nil, fmt.Errorf("could not create numerator for %s: %w", metricSLONumeratorCorrection, err) + } + + denominatorWindow := promutils.TimeDurationToPromStr(totalWindow) + var denominatorBuffer bytes.Buffer + err = tpl.Execute(&denominatorBuffer, map[string]string{ + tplKeyWindow: denominatorWindow, + }) + if err != nil { + return nil, fmt.Errorf("could not create denominator for %s: %w", metricSLONumeratorCorrection, err) + } + + return &rulefmt.Rule{ + Record: metricSLONumeratorCorrection, + Expr: fmt.Sprintf(`(%s)/(%s)`, numeratorBuffer.String(), denominatorBuffer.String()), + Labels: labels, + }, nil +} diff --git a/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin_test.go b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin_test.go new file mode 100644 index 00000000..94f7732e --- /dev/null +++ b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin_test.go @@ -0,0 +1,385 @@ +package plugin_test + +import ( + "testing" + "time" + + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + plugin "github.com/slok/sloth/internal/plugin/slo/contrib/denominator_corrected_rules_v1" + "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" +) + +func baseAlertGroup() model.MWMBAlertGroup { + return model.MWMBAlertGroup{ + PageQuick: model.MWMBAlert{ + ShortWindow: 5 * time.Minute, + LongWindow: 1 * time.Hour, + }, + PageSlow: model.MWMBAlert{ + ShortWindow: 30 * time.Minute, + LongWindow: 6 * time.Hour, + }, + TicketQuick: model.MWMBAlert{ + ShortWindow: 2 * time.Hour, + LongWindow: 1 * 24 * time.Hour, + }, + TicketSlow: model.MWMBAlert{ + ShortWindow: 6 * time.Hour, + LongWindow: 3 * 24 * time.Hour, + }, + } +} + +func baseSLO() model.PromSLO { + return model.PromSLO{ + ID: "svc01-slo1", + Name: "slo1", + Service: "svc01", + TimeWindow: 30 * 24 * time.Hour, + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}]))`, + TotalQuery: `sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}]))`, + }, + }, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + }, + } +} + +func TestProcessSLO(t *testing.T) { + tests := map[string]struct { + req pluginslov1.Request + expRes pluginslov1.Result + expErr bool + }{ + "Having and SLO with invalid expression should fail.": { + req: pluginslov1.Request{ + SLO: model.PromSLO{ + ID: "test", + Name: "test-name", + Service: "test-svc", + TimeWindow: 30 * 24 * time.Hour, + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `rate(my_metric[{{}.window}}]{error="true"})`, + TotalQuery: `rate(my_metric[{{.window}}])`, + }, + }, + Labels: map[string]string{ + "kind": "test", + }, + }, + MWMBAlertGroup: baseAlertGroup(), + }, + expErr: true, + }, + + "Having and wrong variable in the expression should fail.": { + req: pluginslov1.Request{ + SLO: model.PromSLO{ + ID: "test", + Name: "test-name", + Service: "test-svc", + TimeWindow: 30 * 24 * time.Hour, + SLI: model.PromSLI{ + Events: &model.PromSLIEvents{ + ErrorQuery: `rate(my_metric[{{.Window}}]{error="true"})`, + TotalQuery: `rate(my_metric[{{.window}}])`, + }, + }, + Labels: map[string]string{ + "kind": "test", + }, + }, + MWMBAlertGroup: baseAlertGroup(), + }, + expErr: true, + }, + + "Having an SLO with raw SLI, should fail.": { + req: pluginslov1.Request{ + SLO: model.PromSLO{ + ID: "test", + Name: "test-name", + Service: "test-svc", + TimeWindow: 30 * 24 * time.Hour, + SLI: model.PromSLI{ + Raw: &model.PromSLIRaw{ + ErrorRatioQuery: `rate(my_metric[{{.window}}])`, + }, + }, + }, + MWMBAlertGroup: baseAlertGroup(), + }, + + expErr: true, + }, + + "Having an SLO with SLI and its mwmb alerts should create the recording rules.": { + req: pluginslov1.Request{ + SLO: baseSLO(), + MWMBAlertGroup: baseAlertGroup(), + }, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Expr: "(\nslo:numerator_correction:ratio5m{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[5m]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[5m])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "5m", + }, + }, + { + Record: "slo:sli_error:ratio_rate30m", + Expr: "(\nslo:numerator_correction:ratio30m{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[30m]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[30m])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "30m", + }, + }, + { + Record: "slo:sli_error:ratio_rate1h", + Expr: "(\nslo:numerator_correction:ratio1h{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[1h]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[1h])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "1h", + }, + }, + { + Record: "slo:sli_error:ratio_rate2h", + Expr: "(\nslo:numerator_correction:ratio2h{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[2h]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[2h])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "2h", + }, + }, + { + Record: "slo:sli_error:ratio_rate6h", + Expr: "(\nslo:numerator_correction:ratio6h{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[6h]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[6h])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "6h", + }, + }, + { + Record: "slo:sli_error:ratio_rate1d", + Expr: "(\nslo:numerator_correction:ratio1d{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[1d]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[1d])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "1d", + }, + }, + { + Record: "slo:sli_error:ratio_rate3d", + Expr: "(\nslo:numerator_correction:ratio3d{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[3d]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[3d])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "3d", + }, + }, + { + Record: "slo:sli_error:ratio_rate30d", + Expr: "(\nslo:numerator_correction:ratio30d{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[30d]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[30d])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "30d", + }, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:numerator_correction:ratio5m", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[5m])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio30m", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[30m])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio1h", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[1h])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio2h", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[2h])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio6h", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[6h])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio1d", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[1d])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio3d", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[3d])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio30d", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + }}, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + // Load plugin. + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{}) + require.NoError(err) + + // Execute plugin. + gotRes := pluginslov1.Result{} + err = plugin.ProcessSLO(t.Context(), &test.req, &gotRes) + + // Check result. + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, gotRes) + } + }) + } +} + +func BenchmarkPluginYaegi(b *testing.B) { + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{ + PluginConfiguration: []byte("{}"), + }) + if err != nil { + b.Fatal(err) + } + + req := &pluginslov1.Request{ + SLO: baseSLO(), + MWMBAlertGroup: baseAlertGroup(), + } + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), req, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPluginGo(b *testing.B) { + plugin, err := plugin.NewPlugin([]byte("{}"), pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } + + req := &pluginslov1.Request{ + SLO: baseSLO(), + MWMBAlertGroup: baseAlertGroup(), + } + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), req, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} From 009f49a7b2824f60482e1025f7fe4d6731f07188 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Gallego Date: Wed, 24 Sep 2025 13:13:18 +0200 Subject: [PATCH 086/173] Relax stale issues --- .github/workflows/close-stale.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/close-stale.yaml b/.github/workflows/close-stale.yaml index 8efd77a8..3e8e77d3 100644 --- a/.github/workflows/close-stale.yaml +++ b/.github/workflows/close-stale.yaml @@ -9,8 +9,8 @@ jobs: steps: - uses: actions/stale@v10 with: - days-before-stale: 60 - days-before-close: 15 + days-before-stale: 90 + days-before-close: 30 stale-issue-message: "This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 15 days." close-issue-message: "This issue was closed because it has been stale for 15 days with no activity." stale-pr-message: "This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 15 days." From 05b78f510fb3823b87bf8523f485c509dbe6a637 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Wed, 24 Sep 2025 17:35:22 +0200 Subject: [PATCH 087/173] Update deps Signed-off-by: Xabier Larrakoetxea --- deploy/kubernetes/helm/sloth/tests/go.mod | 141 ++++---- deploy/kubernetes/helm/sloth/tests/go.sum | 418 ++++++++++------------ 2 files changed, 247 insertions(+), 312 deletions(-) diff --git a/deploy/kubernetes/helm/sloth/tests/go.mod b/deploy/kubernetes/helm/sloth/tests/go.mod index 6b224593..0560edf6 100644 --- a/deploy/kubernetes/helm/sloth/tests/go.mod +++ b/deploy/kubernetes/helm/sloth/tests/go.mod @@ -1,64 +1,59 @@ module github.com/slok/sloth/helm -go 1.24.0 +go 1.25.0 require ( - github.com/slok/go-helm-template v0.8.0 - github.com/stretchr/testify v1.10.0 + github.com/slok/go-helm-template v0.9.0 + github.com/stretchr/testify v1.11.1 ) require ( - dario.cat/mergo v1.0.1 // indirect - github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect + dario.cat/mergo v1.0.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.3 // indirect - github.com/containerd/containerd v1.7.27 // indirect + github.com/containerd/containerd v1.7.28 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v28.0.2+incompatible // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v28.0.2+incompatible // indirect - github.com/docker/docker-credential-helpers v0.9.3 // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-metrics v0.0.1 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fatih/color v1.18.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.22.0 // indirect + github.com/go-openapi/jsonreference v0.21.1 // indirect + github.com/go-openapi/swag v0.24.1 // indirect + github.com/go-openapi/swag/cmdutils v0.24.0 // indirect + github.com/go-openapi/swag/conv v0.24.0 // indirect + github.com/go-openapi/swag/fileutils v0.24.0 // indirect + github.com/go-openapi/swag/jsonname v0.24.0 // indirect + github.com/go-openapi/swag/jsonutils v0.24.0 // indirect + github.com/go-openapi/swag/loading v0.24.0 // indirect + github.com/go-openapi/swag/mangling v0.24.0 // indirect + github.com/go-openapi/swag/netutils v0.24.0 // indirect + github.com/go-openapi/swag/stringutils v0.24.0 // indirect + github.com/go-openapi/swag/typeutils v0.24.0 // indirect + github.com/go-openapi/swag/yamlutils v0.24.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.5.3 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -73,18 +68,17 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect @@ -93,59 +87,50 @@ require ( github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.21.1 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.63.0 // indirect - github.com/prometheus/procfs v0.16.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rubenv/sql-migrate v1.7.1 // indirect + github.com/rubenv/sql-migrate v1.8.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.37.0 // indirect - golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.11.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.71.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/time v0.13.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect + google.golang.org/grpc v1.75.1 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - helm.sh/helm/v3 v3.17.2 // indirect - k8s.io/api v0.32.3 // indirect - k8s.io/apiextensions-apiserver v0.32.3 // indirect - k8s.io/apimachinery v0.32.3 // indirect - k8s.io/apiserver v0.32.3 // indirect - k8s.io/cli-runtime v0.32.3 // indirect - k8s.io/client-go v0.32.3 // indirect - k8s.io/component-base v0.32.3 // indirect + helm.sh/helm/v3 v3.19.0 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect + k8s.io/apimachinery v0.34.1 // indirect + k8s.io/apiserver v0.34.1 // indirect + k8s.io/cli-runtime v0.34.1 // indirect + k8s.io/client-go v0.34.1 // indirect + k8s.io/component-base v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/kubectl v0.32.3 // indirect - k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect - oras.land/oras-go v1.2.6 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect - sigs.k8s.io/kustomize/api v0.19.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/kubectl v0.34.1 // indirect + k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect + oras.land/oras-go/v2 v2.6.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/deploy/kubernetes/helm/sloth/tests/go.sum b/deploy/kubernetes/helm/sloth/tests/go.sum index 12745c67..8ed52ae3 100644 --- a/deploy/kubernetes/helm/sloth/tests/go.sum +++ b/deploy/kubernetes/helm/sloth/tests/go.sum @@ -1,5 +1,5 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= @@ -14,24 +14,16 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.12.3 h1:LS9NXqXhMoqNCplK1ApmVSfB4UnVLRDWRapB6EIlxE0= -github.com/Microsoft/hcsshim v0.12.3/go.mod h1:Iyl1WVpZzr+UkzjekHZbV8o5Z9ZkxNGx6CtY2Qg/JVQ= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -44,13 +36,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= -github.com/containerd/cgroups/v3 v3.0.2 h1:f5WFqIVSgo5IZmtTT3qVBo6TzI1ON6sycSBKkymb9L0= -github.com/containerd/cgroups/v3 v3.0.2/go.mod h1:JUgITrzdFqp42uI2ryGA+ge0ap/nxzYgkGmIcetmErE= -github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= -github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0= -github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= -github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/containerd v1.7.28 h1:Nsgm1AtcmEh4AHAJ4gGlNSaKgXiNccU270Dnf81FQ3c= +github.com/containerd/containerd v1.7.28/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= @@ -70,28 +57,20 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/distribution/v3 v3.0.0-beta.1 h1:X+ELTxPuZ1Xe5MsD3kp2wfGUhc8I+MPfRis8dZ818Ic= -github.com/distribution/distribution/v3 v3.0.0-beta.1/go.mod h1:O9O8uamhHzWWQVTjuQpyYUVm/ShPHPUDgvQMpHGVBDs= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v28.0.2+incompatible h1:cRPZ77FK3/IXTAIQQj1vmhlxiLS5m+MIUDwS6f57lrE= -github.com/docker/cli v28.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.0.2+incompatible h1:9BILleFwug5FSSqWBgVevgL3ewDJfWWWyZVqlDMttE8= -github.com/docker/docker v28.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= @@ -104,79 +83,82 @@ github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7Dlme github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= +github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= +github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= +github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= +github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= +github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= +github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= +github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= +github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= +github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= +github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= +github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= +github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= +github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= +github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= +github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= +github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 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.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= @@ -189,23 +171,16 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 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.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= @@ -214,8 +189,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -224,7 +199,6 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= @@ -233,28 +207,20 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= 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= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= @@ -269,7 +235,6 @@ github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+v github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -277,232 +242,217 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= -github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= -github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= -github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4= -github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4= +github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= +github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/slok/go-helm-template v0.8.0 h1:rERtZLpbwGpeiAd9RaHPoshGL821obT/cBhfxkayXdo= -github.com/slok/go-helm-template v0.8.0/go.mod h1:RB11tKCmcV/3/oKRuxCfM7ImumY8kZ7vVluetwBVc/c= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/slok/go-helm-template v0.9.0 h1:jH2/H7MlvyP439PM6k4izLs34s3L6V1bEKUdtPhR0GY= +github.com/slok/go-helm-template v0.9.0/go.mod h1:hscWXfs9ZhHYiYBTx9GXut/6cMpTvjLCdRafWyc8P2I= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 h1:ysCfPZB9AjUlMa1UHYup3c9dAOCMQX/6sxSfPBUoxHw= -go.opentelemetry.io/contrib/exporters/autoexport v0.46.1/go.mod h1:ha0aiYm+DOPsLHjh0zoQ8W8sLT+LJ58J3j47lGpSLrU= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 h1:bflGWrfYyuulcdxf14V6n9+CoQcu5SAAdHmDPAJnlps= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0/go.mod h1:qcTO4xHAxZLaLxPd60TdE88rxtItPHgHWqOhOGRr0as= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= -go.opentelemetry.io/otel/exporters/prometheus v0.44.0 h1:08qeJgaPC0YEBu2PQMbqU3rogTlyzpjhCI2b58Yn00w= -go.opentelemetry.io/otel/exporters/prometheus v0.44.0/go.mod h1:ERL2uIeBtg4TxZdojHUwzZfIFlUIjZtxubT5p4h1Gjg= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 h1:dEZWPjVN22urgYCza3PXRUGEyCB++y1sAqm6guWFesk= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0/go.mod h1:sTt30Evb7hJB/gEk27qLb1+l9n4Tb8HvHkR0Wx3S6CU= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= 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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 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= -google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= -google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= -google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= +google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 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.1/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= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -helm.sh/helm/v3 v3.17.2 h1:agYQ5ew2jq5vdx2K7q5W44KyKQrnSubUMCQsjkiv3/o= -helm.sh/helm/v3 v3.17.2/go.mod h1:+uJKMH/UiMzZQOALR3XUf3BLIoczI2RKKD6bMhPh4G8= -k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= -k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= -k8s.io/apiextensions-apiserver v0.32.3 h1:4D8vy+9GWerlErCwVIbcQjsWunF9SUGNu7O7hiQTyPY= -k8s.io/apiextensions-apiserver v0.32.3/go.mod h1:8YwcvVRMVzw0r1Stc7XfGAzB/SIVLunqApySV5V7Dss= -k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= -k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.3 h1:kOw2KBuHOA+wetX1MkmrxgBr648ksz653j26ESuWNY8= -k8s.io/apiserver v0.32.3/go.mod h1:q1x9B8E/WzShF49wh3ADOh6muSfpmFL0I2t+TG0Zdgc= -k8s.io/cli-runtime v0.32.3 h1:khLF2ivU2T6Q77H97atx3REY9tXiA3OLOjWJxUrdvss= -k8s.io/cli-runtime v0.32.3/go.mod h1:vZT6dZq7mZAca53rwUfdFSZjdtLyfF61mkf/8q+Xjak= -k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= -k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= -k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= -k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= +helm.sh/helm/v3 v3.19.0 h1:krVyCGa8fa/wzTZgqw0DUiXuRT5BPdeqE/sQXujQ22k= +helm.sh/helm/v3 v3.19.0/go.mod h1:Lk/SfzN0w3a3C3o+TdAKrLwJ0wcZ//t1/SDXAvfgDdc= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M= +k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= +k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/kubectl v0.32.3 h1:VMi584rbboso+yjfv0d8uBHwwxbC438LKq+dXd5tOAI= -k8s.io/kubectl v0.32.3/go.mod h1:6Euv2aso5GKzo/UVMacV6C7miuyevpfI91SvBvV9Zdg= -k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= -k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go v1.2.6 h1:z8cmxQXBU8yZ4mkytWqXfo6tZcamPwjsuxYU81xJ8Lk= -oras.land/oras-go v1.2.6/go.mod h1:OVPc1PegSEe/K8YiLfosrlqlqTN9PUyFvOw5Y9gwrT8= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= -sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= -sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= -sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/kubectl v0.34.1 h1:1qP1oqT5Xc93K+H8J7ecpBjaz511gan89KO9Vbsh/OI= +k8s.io/kubectl v0.34.1/go.mod h1:JRYlhJpGPyk3dEmJ+BuBiOB9/dAvnrALJEiY/C5qa6A= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= +k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From 390934fe1656354c20e02ac070eaf934ddd5af09 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Thu, 25 Sep 2025 17:38:35 +0200 Subject: [PATCH 088/173] Improve conventions Signed-off-by: Xabier Larrakoetxea --- .../denominator_corrected_rules_v1/plugin.go | 23 ++++++------ .../error_budget_exhausted_alert_v1/plugin.go | 9 ++--- .../slo/contrib/info_labels_v1/plugin.go | 3 +- .../validate_victoria_metrics_v1/plugin.go | 3 +- .../slo/core/metadata_rules_v1/plugin.go | 31 ++++++---------- .../plugin/slo/core/sli_rules_v1/plugin.go | 8 ++--- ...com-prometheus-prometheus-promql-parser.go | 17 ++++----- ...b_com-slok-sloth-pkg-common-conventions.go | 36 +++++++++++-------- pkg/common/conventions/conventions.go | 7 ++-- pkg/common/conventions/slo.go | 11 +++++- pkg/common/validation/slo.go | 2 +- 11 files changed, 80 insertions(+), 70 deletions(-) diff --git a/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go index bd1ea4f2..0b943ece 100644 --- a/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go +++ b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go @@ -22,6 +22,10 @@ const ( PluginID = "sloth.dev/contrib/denominator_corrected_rules/v1" ) +const ( + numeratorCorrectionMetric = "slo:numerator_correction:ratio" // The correction factor metric name. +) + type PluginConfig struct{} func NewPlugin(c json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { @@ -109,7 +113,7 @@ func getAlertGroupWindows(alerts model.MWMBAlertGroup) []time.Duration { func denominatorCorrectedSLIRecordGenerator(slo model.PromSLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { const sliExprTplFmt = `( -slo:numerator_correction:ratio{{.window}}{{.filter}} +{{.numeratorCorrectionMetric}}{{.window}}{{.filter}} * on() %s ) @@ -129,9 +133,10 @@ slo:numerator_correction:ratio{{.window}}{{.filter}} var b bytes.Buffer err = tpl.Execute(&b, map[string]string{ - tplKeyWindow: strWindow, - "filter": promutils.LabelsToPromFilter(conventions.GetSLOIDPromLabels(slo)), - "windowKey": conventions.PromSLOWindowLabelName, + "numeratorCorrectionMetric": numeratorCorrectionMetric, + "window": strWindow, + "filter": promutils.LabelsToPromFilter(conventions.GetSLOIDPromLabels(slo)), + "windowKey": conventions.PromSLOWindowLabelName, }) if err != nil { return nil, fmt.Errorf("could not render SLI expression template: %w", err) @@ -150,13 +155,9 @@ slo:numerator_correction:ratio{{.window}}{{.filter}} }, nil } -const ( - tplKeyWindow = "window" -) - func createNumeratorCorrection(slo model.PromSLO, labels map[string]string, currentWindow, totalWindow time.Duration) (*rulefmt.Rule, error) { windowString := promutils.TimeDurationToPromStr(currentWindow) - metricSLONumeratorCorrection := fmt.Sprintf("slo:numerator_correction:ratio%s", windowString) + metricSLONumeratorCorrection := numeratorCorrectionMetric + windowString tpl, err := template.New("sliExpr").Option("missingkey=error").Parse(slo.SLI.Events.TotalQuery) if err != nil { @@ -165,7 +166,7 @@ func createNumeratorCorrection(slo model.PromSLO, labels map[string]string, curr var numeratorBuffer bytes.Buffer err = tpl.Execute(&numeratorBuffer, map[string]string{ - tplKeyWindow: windowString, + conventions.TplSLIQueryWindowVarName: windowString, }) if err != nil { return nil, fmt.Errorf("could not create numerator for %s: %w", metricSLONumeratorCorrection, err) @@ -174,7 +175,7 @@ func createNumeratorCorrection(slo model.PromSLO, labels map[string]string, curr denominatorWindow := promutils.TimeDurationToPromStr(totalWindow) var denominatorBuffer bytes.Buffer err = tpl.Execute(&denominatorBuffer, map[string]string{ - tplKeyWindow: denominatorWindow, + conventions.TplSLIQueryWindowVarName: denominatorWindow, }) if err != nil { return nil, fmt.Errorf("could not create denominator for %s: %w", metricSLONumeratorCorrection, err) diff --git a/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin.go b/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin.go index 2e254d18..056fd4a6 100644 --- a/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin.go +++ b/internal/plugin/slo/contrib/error_budget_exhausted_alert_v1/plugin.go @@ -9,6 +9,7 @@ import ( "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/rulefmt" + "github.com/slok/sloth/pkg/common/conventions" pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" ) @@ -66,9 +67,9 @@ func (p plugin) ProcessSLO(_ context.Context, req *pluginslov1.Request, result * // Base labels for the alert labels := map[string]string{ - "sloth_slo": slo.Name, - "sloth_service": slo.Service, - "sloth_id": fmt.Sprintf("%s-%s", slo.Service, slo.Name), + conventions.PromSLONameLabelName: slo.Name, + conventions.PromSLOServiceLabelName: slo.Service, + conventions.PromSLOIDLabelName: fmt.Sprintf("%s-%s", slo.Service, slo.Name), } // Add all SLO custom labels @@ -81,7 +82,7 @@ func (p plugin) ProcessSLO(_ context.Context, req *pluginslov1.Request, result * labels[k] = v } - expr := fmt.Sprintf(`slo:period_error_budget_remaining:ratio%s <= %g`, labelMatcher(labels), p.config.Threshold) + expr := fmt.Sprintf(`%s%s <= %g`, conventions.PromMetaSLOPeriodErrorBudgetRemainingRatioMetric, labelMatcher(labels), p.config.Threshold) // Alert annotations mixed in too annotations := make(map[string]string) diff --git a/internal/plugin/slo/contrib/info_labels_v1/plugin.go b/internal/plugin/slo/contrib/info_labels_v1/plugin.go index b683ac74..0481327e 100644 --- a/internal/plugin/slo/contrib/info_labels_v1/plugin.go +++ b/internal/plugin/slo/contrib/info_labels_v1/plugin.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/slok/sloth/pkg/common/conventions" utilsdata "github.com/slok/sloth/pkg/common/utils/data" pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" ) @@ -27,7 +28,7 @@ func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1. } if config.MetricName == "" { - config.MetricName = "sloth_slo_info" // Default info label. + config.MetricName = conventions.PromMetaSLOInfoMetric } if len(config.Labels) == 0 { diff --git a/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go index e55c44d8..57e5e0c1 100644 --- a/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go +++ b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/plugin.go @@ -9,6 +9,7 @@ import ( "github.com/VictoriaMetrics/metricsql" + "github.com/slok/sloth/pkg/common/conventions" "github.com/slok/sloth/pkg/common/validation" pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" ) @@ -46,7 +47,7 @@ type victoriaMetricsDialectValidator struct { promValidator validation.SLODialectValidator } -var promExprTplAllowedFakeData = map[string]string{"window": "1m"} +var promExprTplAllowedFakeData = map[string]string{conventions.TplSLIQueryWindowVarName: "1m"} func (v victoriaMetricsDialectValidator) ValidateLabelKey(key string) error { return v.promValidator.ValidateLabelKey(key) diff --git a/internal/plugin/slo/core/metadata_rules_v1/plugin.go b/internal/plugin/slo/core/metadata_rules_v1/plugin.go index 65287de9..3013b98b 100644 --- a/internal/plugin/slo/core/metadata_rules_v1/plugin.go +++ b/internal/plugin/slo/core/metadata_rules_v1/plugin.go @@ -39,17 +39,6 @@ func (plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, resu func generateMetadataRecordingRules(ctx context.Context, info model.Info, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { labels := utilsdata.MergeLabels(conventions.GetSLOIDPromLabels(slo), slo.Labels) - // Metatada Recordings. - const ( - metricSLOObjectiveRatio = "slo:objective:ratio" - metricSLOErrorBudgetRatio = "slo:error_budget:ratio" - metricSLOTimePeriodDays = "slo:time_period:days" - metricSLOCurrentBurnRateRatio = "slo:current_burn_rate:ratio" - metricSLOPeriodBurnRateRatio = "slo:period_burn_rate:ratio" - metricSLOPeriodErrorBudgetRemainingRatio = "slo:period_error_budget_remaining:ratio" - metricSLOInfo = "sloth_slo_info" - ) - sloObjectiveRatio := slo.Objective / 100 sloFilter := promutils.LabelsToPromFilter(conventions.GetSLOIDPromLabels(slo)) @@ -61,7 +50,7 @@ func generateMetadataRecordingRules(ctx context.Context, info model.Info, slo mo "SLOIDName": conventions.PromSLOIDLabelName, "SLOLabelName": conventions.PromSLONameLabelName, "SLOServiceName": conventions.PromSLOServiceLabelName, - "ErrorBudgetRatioMetric": metricSLOErrorBudgetRatio, + "ErrorBudgetRatioMetric": conventions.PromMetaSLOErrorBudgetRatioMetric, }) if err != nil { return nil, fmt.Errorf("could not render current burn rate prometheus metadata recording rule expression: %w", err) @@ -74,7 +63,7 @@ func generateMetadataRecordingRules(ctx context.Context, info model.Info, slo mo "SLOIDName": conventions.PromSLOIDLabelName, "SLOLabelName": conventions.PromSLONameLabelName, "SLOServiceName": conventions.PromSLOServiceLabelName, - "ErrorBudgetRatioMetric": metricSLOErrorBudgetRatio, + "ErrorBudgetRatioMetric": conventions.PromMetaSLOErrorBudgetRatioMetric, }) if err != nil { return nil, fmt.Errorf("could not render period burn rate prometheus metadata recording rule expression: %w", err) @@ -83,49 +72,49 @@ func generateMetadataRecordingRules(ctx context.Context, info model.Info, slo mo rules := []rulefmt.Rule{ // SLO Objective. { - Record: metricSLOObjectiveRatio, + Record: conventions.PromMetaSLOObjectiveRatioMetric, Expr: fmt.Sprintf(`vector(%g)`, sloObjectiveRatio), Labels: labels, }, // Error budget. { - Record: metricSLOErrorBudgetRatio, + Record: conventions.PromMetaSLOErrorBudgetRatioMetric, Expr: fmt.Sprintf(`vector(1-%g)`, sloObjectiveRatio), Labels: labels, }, // Total period. { - Record: metricSLOTimePeriodDays, + Record: conventions.PromMetaSLOTimePeriodDaysMetric, Expr: fmt.Sprintf(`vector(%g)`, slo.TimeWindow.Hours()/24), Labels: labels, }, // Current burning speed. { - Record: metricSLOCurrentBurnRateRatio, + Record: conventions.PromMetaSLOCurrentBurnRateRatioMetric, Expr: currentBurnRateExpr.String(), Labels: labels, }, // Total period burn rate. { - Record: metricSLOPeriodBurnRateRatio, + Record: conventions.PromMetaSLOPeriodBurnRateRatioMetric, Expr: periodBurnRateExpr.String(), Labels: labels, }, // Total Error budget remaining period. { - Record: metricSLOPeriodErrorBudgetRemainingRatio, - Expr: fmt.Sprintf(`1 - %s%s`, metricSLOPeriodBurnRateRatio, sloFilter), + Record: conventions.PromMetaSLOPeriodErrorBudgetRemainingRatioMetric, + Expr: fmt.Sprintf(`1 - %s%s`, conventions.PromMetaSLOPeriodBurnRateRatioMetric, sloFilter), Labels: labels, }, // Info. { - Record: metricSLOInfo, + Record: conventions.PromMetaSLOInfoMetric, Expr: `vector(1)`, Labels: utilsdata.MergeLabels(labels, map[string]string{ conventions.PromSLOVersionLabelName: info.Version, diff --git a/internal/plugin/slo/core/sli_rules_v1/plugin.go b/internal/plugin/slo/core/sli_rules_v1/plugin.go index c41dfc0a..64cb9527 100644 --- a/internal/plugin/slo/core/sli_rules_v1/plugin.go +++ b/internal/plugin/slo/core/sli_rules_v1/plugin.go @@ -88,10 +88,6 @@ func optimizedFactorySLIRecordGenerator(slo model.PromSLO, window time.Duration, return factorySLIRecordGenerator(slo, window, alerts) } -const ( - tplKeyWindow = "window" -) - // factorySLIRecordGenerator knows how to generate the SLI prometheus recording rules // form an SLO. // Normally these rules are used by the SLO alerts. @@ -119,7 +115,7 @@ func rawSLIRecordGenerator(slo model.PromSLO, window time.Duration, alerts model strWindow := promutils.TimeDurationToPromStr(window) var b bytes.Buffer err = tpl.Execute(&b, map[string]string{ - tplKeyWindow: strWindow, + conventions.TplSLIQueryWindowVarName: strWindow, }) if err != nil { return nil, fmt.Errorf("could not render SLI expression template: %w", err) @@ -155,7 +151,7 @@ func eventsSLIRecordGenerator(slo model.PromSLO, window time.Duration, alerts mo strWindow := promutils.TimeDurationToPromStr(window) var b bytes.Buffer err = tpl.Execute(&b, map[string]string{ - tplKeyWindow: strWindow, + conventions.TplSLIQueryWindowVarName: strWindow, }) if err != nil { return nil, fmt.Errorf("could not render SLI expression template: %w", err) diff --git a/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go b/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go index 56acf3d8..97165e4b 100644 --- a/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go +++ b/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go @@ -27,7 +27,7 @@ func init() { "COMMA": reflect.ValueOf(constant.MakeFromLiteral("57349", token.INT, 0)), "COMMENT": reflect.ValueOf(constant.MakeFromLiteral("57350", token.INT, 0)), "COUNT": reflect.ValueOf(constant.MakeFromLiteral("57406", token.INT, 0)), - "COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57435", token.INT, 0)), + "COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57436", token.INT, 0)), "COUNTER_RESET_HINT_DESC": reflect.ValueOf(constant.MakeFromLiteral("57380", token.INT, 0)), "COUNT_DESC": reflect.ValueOf(constant.MakeFromLiteral("57371", token.INT, 0)), "COUNT_VALUES": reflect.ValueOf(constant.MakeFromLiteral("57407", token.INT, 0)), @@ -51,7 +51,7 @@ func init() { "ExperimentalDurationExpr": reflect.ValueOf(&parser.ExperimentalDurationExpr).Elem(), "ExtractSelectors": reflect.ValueOf(parser.ExtractSelectors), "Functions": reflect.ValueOf(&parser.Functions).Elem(), - "GAUGE_TYPE": reflect.ValueOf(constant.MakeFromLiteral("57437", token.INT, 0)), + "GAUGE_TYPE": reflect.ValueOf(constant.MakeFromLiteral("57438", token.INT, 0)), "GROUP": reflect.ValueOf(constant.MakeFromLiteral("57408", token.INT, 0)), "GROUP_LEFT": reflect.ValueOf(constant.MakeFromLiteral("57422", token.INT, 0)), "GROUP_RIGHT": reflect.ValueOf(constant.MakeFromLiteral("57423", token.INT, 0)), @@ -83,7 +83,7 @@ func init() { "NEGATIVE_OFFSET_DESC": reflect.ValueOf(constant.MakeFromLiteral("57374", token.INT, 0)), "NEQ": reflect.ValueOf(constant.MakeFromLiteral("57396", token.INT, 0)), "NEQ_REGEX": reflect.ValueOf(constant.MakeFromLiteral("57397", token.INT, 0)), - "NOT_COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57436", token.INT, 0)), + "NOT_COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57437", token.INT, 0)), "NUMBER": reflect.ValueOf(constant.MakeFromLiteral("57361", token.INT, 0)), "NewParser": reflect.ValueOf(parser.NewParser), "OFFSET": reflect.ValueOf(constant.MakeFromLiteral("57425", token.INT, 0)), @@ -105,12 +105,13 @@ func init() { "SEMICOLON": reflect.ValueOf(constant.MakeFromLiteral("57365", token.INT, 0)), "SPACE": reflect.ValueOf(constant.MakeFromLiteral("57366", token.INT, 0)), "START": reflect.ValueOf(constant.MakeFromLiteral("57430", token.INT, 0)), - "START_EXPRESSION": reflect.ValueOf(constant.MakeFromLiteral("57442", token.INT, 0)), - "START_METRIC": reflect.ValueOf(constant.MakeFromLiteral("57440", token.INT, 0)), - "START_METRIC_SELECTOR": reflect.ValueOf(constant.MakeFromLiteral("57443", token.INT, 0)), - "START_SERIES_DESCRIPTION": reflect.ValueOf(constant.MakeFromLiteral("57441", token.INT, 0)), + "START_EXPRESSION": reflect.ValueOf(constant.MakeFromLiteral("57443", token.INT, 0)), + "START_METRIC": reflect.ValueOf(constant.MakeFromLiteral("57441", token.INT, 0)), + "START_METRIC_SELECTOR": reflect.ValueOf(constant.MakeFromLiteral("57444", token.INT, 0)), + "START_SERIES_DESCRIPTION": reflect.ValueOf(constant.MakeFromLiteral("57442", token.INT, 0)), "STDDEV": reflect.ValueOf(constant.MakeFromLiteral("57412", token.INT, 0)), "STDVAR": reflect.ValueOf(constant.MakeFromLiteral("57413", token.INT, 0)), + "STEP": reflect.ValueOf(constant.MakeFromLiteral("57432", token.INT, 0)), "STRING": reflect.ValueOf(constant.MakeFromLiteral("57367", token.INT, 0)), "SUB": reflect.ValueOf(constant.MakeFromLiteral("57399", token.INT, 0)), "SUM": reflect.ValueOf(constant.MakeFromLiteral("57414", token.INT, 0)), @@ -118,7 +119,7 @@ func init() { "TIMES": reflect.ValueOf(constant.MakeFromLiteral("57368", token.INT, 0)), "TOPK": reflect.ValueOf(constant.MakeFromLiteral("57415", token.INT, 0)), "Tree": reflect.ValueOf(parser.Tree), - "UNKNOWN_COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57434", token.INT, 0)), + "UNKNOWN_COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57435", token.INT, 0)), "ValueTypeMatrix": reflect.ValueOf(parser.ValueTypeMatrix), "ValueTypeNone": reflect.ValueOf(parser.ValueTypeNone), "ValueTypeScalar": reflect.ValueOf(parser.ValueTypeScalar), diff --git a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go index bbc66b2b..7bdcd30d 100644 --- a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go +++ b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go @@ -12,19 +12,27 @@ import ( func init() { Symbols["github.com/slok/sloth/pkg/common/conventions/conventions"] = map[string]reflect.Value{ // function, constant and variable definitions - "GetSLIErrorMetric": reflect.ValueOf(conventions.GetSLIErrorMetric), - "GetSLOIDPromLabels": reflect.ValueOf(conventions.GetSLOIDPromLabels), - "NameRegexp": reflect.ValueOf(&conventions.NameRegexp).Elem(), - "PromSLIErrorMetricFmt": reflect.ValueOf(constant.MakeFromLiteral("\"slo:sli_error:ratio_rate%s\"", token.STRING, 0)), - "PromSLOIDLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_id\"", token.STRING, 0)), - "PromSLOModeLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_mode\"", token.STRING, 0)), - "PromSLONameLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_slo\"", token.STRING, 0)), - "PromSLOObjectiveLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_objective\"", token.STRING, 0)), - "PromSLOServiceLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_service\"", token.STRING, 0)), - "PromSLOSeverityLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_severity\"", token.STRING, 0)), - "PromSLOSpecLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_spec\"", token.STRING, 0)), - "PromSLOVersionLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_version\"", token.STRING, 0)), - "PromSLOWindowLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_window\"", token.STRING, 0)), - "TplWindowRegex": reflect.ValueOf(&conventions.TplWindowRegex).Elem(), + "GetSLIErrorMetric": reflect.ValueOf(conventions.GetSLIErrorMetric), + "GetSLOIDPromLabels": reflect.ValueOf(conventions.GetSLOIDPromLabels), + "NameRegexp": reflect.ValueOf(&conventions.NameRegexp).Elem(), + "PromMetaSLOCurrentBurnRateRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:current_burn_rate:ratio\"", token.STRING, 0)), + "PromMetaSLOErrorBudgetRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:error_budget:ratio\"", token.STRING, 0)), + "PromMetaSLOInfoMetric": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_slo_info\"", token.STRING, 0)), + "PromMetaSLOObjectiveRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:objective:ratio\"", token.STRING, 0)), + "PromMetaSLOPeriodBurnRateRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:period_burn_rate:ratio\"", token.STRING, 0)), + "PromMetaSLOPeriodErrorBudgetRemainingRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:period_error_budget_remaining:ratio\"", token.STRING, 0)), + "PromMetaSLOTimePeriodDaysMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:time_period:days\"", token.STRING, 0)), + "PromSLIErrorMetricFmt": reflect.ValueOf(constant.MakeFromLiteral("\"slo:sli_error:ratio_rate%s\"", token.STRING, 0)), + "PromSLOIDLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_id\"", token.STRING, 0)), + "PromSLOModeLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_mode\"", token.STRING, 0)), + "PromSLONameLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_slo\"", token.STRING, 0)), + "PromSLOObjectiveLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_objective\"", token.STRING, 0)), + "PromSLOServiceLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_service\"", token.STRING, 0)), + "PromSLOSeverityLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_severity\"", token.STRING, 0)), + "PromSLOSpecLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_spec\"", token.STRING, 0)), + "PromSLOVersionLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_version\"", token.STRING, 0)), + "PromSLOWindowLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_window\"", token.STRING, 0)), + "TplSLIQueryWindowVarName": reflect.ValueOf(&conventions.TplSLIQueryWindowVarName).Elem(), + "TplSLIQueryWindowVarRegex": reflect.ValueOf(&conventions.TplSLIQueryWindowVarRegex).Elem(), } } diff --git a/pkg/common/conventions/conventions.go b/pkg/common/conventions/conventions.go index ff07e61a..68772cbd 100644 --- a/pkg/common/conventions/conventions.go +++ b/pkg/common/conventions/conventions.go @@ -9,6 +9,9 @@ var ( // - Contain alphanumeric, `.`, '_', and '-'. NameRegexp = regexp.MustCompile(`^[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]$`) - // TplWindowRegex is the regex to match the {{ .window }} template variable. - TplWindowRegex = regexp.MustCompile(`{{ *\.window *}}`) + // TplSLIQueryWindowVarRegex is the regex to match the {{ .window }} template variable used in the SLI queries. + TplSLIQueryWindowVarRegex = regexp.MustCompile(`{{ *\.window *}}`) + + // TplSLIQueryWindowVarName is the name of the window template variable used in the SLI queries. + TplSLIQueryWindowVarName = "window" ) diff --git a/pkg/common/conventions/slo.go b/pkg/common/conventions/slo.go index 03b88c64..9d030efc 100644 --- a/pkg/common/conventions/slo.go +++ b/pkg/common/conventions/slo.go @@ -4,9 +4,18 @@ import "github.com/slok/sloth/pkg/common/model" // Prometheus metrics conventions. const ( - // Metrics. + // Metrics SLI. PromSLIErrorMetricFmt = "slo:sli_error:ratio_rate%s" + // Metrics meta. + PromMetaSLOObjectiveRatioMetric = "slo:objective:ratio" + PromMetaSLOErrorBudgetRatioMetric = "slo:error_budget:ratio" + PromMetaSLOTimePeriodDaysMetric = "slo:time_period:days" + PromMetaSLOCurrentBurnRateRatioMetric = "slo:current_burn_rate:ratio" + PromMetaSLOPeriodBurnRateRatioMetric = "slo:period_burn_rate:ratio" + PromMetaSLOPeriodErrorBudgetRemainingRatioMetric = "slo:period_error_budget_remaining:ratio" + PromMetaSLOInfoMetric = "sloth_slo_info" + // Labels. PromSLONameLabelName = "sloth_slo" PromSLOIDLabelName = "sloth_id" diff --git a/pkg/common/validation/slo.go b/pkg/common/validation/slo.go index 0e741d43..f2bb0345 100644 --- a/pkg/common/validation/slo.go +++ b/pkg/common/validation/slo.go @@ -25,7 +25,7 @@ func isValidSLIQueryTemplate(sliQuery string) error { return fmt.Errorf("query template is required: %w", commonerrors.ErrRequired) } - if !conventions.TplWindowRegex.MatchString(sliQuery) { + if !conventions.TplSLIQueryWindowVarRegex.MatchString(sliQuery) { return fmt.Errorf("template must contain the {{ .window }} variable") } From 4612c185d7aa2af2ea141bd6028cfd0717bcda23 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Wed, 1 Oct 2025 18:00:20 +0200 Subject: [PATCH 089/173] Add optimized rules to denonimator corrected plugin Signed-off-by: Xabier Larrakoetxea --- .../_gen/contrib-denominator-corrected.yaml | 19 +- .../denominator_corrected_rules_v1/README.md | 13 +- .../denominator_corrected_rules_v1/plugin.go | 92 ++++---- .../plugin_test.go | 209 +++++++++++++++++- .../plugin/slo/core/sli_rules_v1/plugin.go | 26 +-- pkg/common/model/alert.go | 29 ++- 6 files changed, 295 insertions(+), 93 deletions(-) diff --git a/examples/_gen/contrib-denominator-corrected.yaml b/examples/_gen/contrib-denominator-corrected.yaml index d5864244..63df0e40 100644 --- a/examples/_gen/contrib-denominator-corrected.yaml +++ b/examples/_gen/contrib-denominator-corrected.yaml @@ -135,13 +135,9 @@ spec: sloth_window: 3d record: slo:sli_error:ratio_rate3d - expr: | - ( - slo:numerator_correction:ratio30d{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"} - * on() - sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[30d])) - ) - / - (sum(rate(http_request_duration_seconds_count{job="myservice"}[30d]))) + sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"}[30d]) + / ignoring (sloth_window) + count_over_time(slo:sli_error:ratio_rate5m{sloth_id="svc01-slo1", sloth_service="svc01", sloth_slo="slo1"}[30d]) labels: cmd: examplesgen.sh global01k1: global01v1 @@ -290,15 +286,6 @@ spec: sloth_service: svc01 sloth_slo: slo1 record: slo:numerator_correction:ratio3d - - expr: (sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d]))) - labels: - cmd: examplesgen.sh - global01k1: global01v1 - global02k1: global02v1 - sloth_id: svc01-slo1 - sloth_service: svc01 - sloth_slo: slo1 - record: slo:numerator_correction:ratio30d - name: sloth-slo-alerts-svc01-slo1 rules: - alert: myServiceAlert diff --git a/internal/plugin/slo/contrib/denominator_corrected_rules_v1/README.md b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/README.md index 9ed42521..114c9271 100644 --- a/internal/plugin/slo/contrib/denominator_corrected_rules_v1/README.md +++ b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/README.md @@ -17,7 +17,7 @@ More details in the original [PR]. ## Config -None +- `disableOptimized`(**Optional**, `bool`): If `true`, disables optimized rule generation for long SLI windows. Optimized rules use short-window recording rules to derive long-window SLIs with lower Prometheus resource usage, at the cost of reduced accuracy. Defaults to `false`. ## Env vars @@ -37,4 +37,15 @@ This plugin should run after rule generation plugins. - id: "sloth.dev/contrib/denominator_corrected_rules/v1" ``` +### Disable optimization + +```yaml +sloPlugins: + chain: + - id: "sloth.dev/contrib/denominator_corrected_rules/v1" + config: + disableOptimized: true +``` + + [PR]: https://github.com/slok/sloth/pull/459 \ No newline at end of file diff --git a/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go index 0b943ece..21b6d6e3 100644 --- a/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go +++ b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "sort" "text/template" "time" @@ -26,7 +25,9 @@ const ( numeratorCorrectionMetric = "slo:numerator_correction:ratio" // The correction factor metric name. ) -type PluginConfig struct{} +type PluginConfig struct { + DisableOptimized bool `json:"disableOptimized,omitempty"` +} func NewPlugin(c json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { cfg := &PluginConfig{} @@ -49,36 +50,31 @@ func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, re } // Generate and override SLI recordings. - sliRules, err := generateSLIRecordingRules(ctx, request.SLO, request.MWMBAlertGroup) + sliRules, err := p.generateSLIRecordingRules(ctx, request.SLO, request.MWMBAlertGroup) if err != nil { return err } result.SLORules.SLIErrorRecRules.Rules = sliRules // Add required new metadata recordings with the correction factor. - windows := getAlertGroupWindows(request.MWMBAlertGroup) - windows = append(windows, request.SLO.TimeWindow) // Add the total time window as a handy helper. - metadataLabels := utilsdata.MergeLabels(conventions.GetSLOIDPromLabels(request.SLO), request.SLO.Labels) - for _, window := range windows { - rule, err := createNumeratorCorrection(request.SLO, metadataLabels, window, request.SLO.TimeWindow) - if err != nil { - return fmt.Errorf("could not create numerator rule: %v", err) - } - result.SLORules.MetadataRecRules.Rules = append(result.SLORules.MetadataRecRules.Rules, *rule) + metaRules, err := p.generateMetaRecordingRules(ctx, request.SLO, request.MWMBAlertGroup) + if err != nil { + return err } + result.SLORules.MetadataRecRules.Rules = append(result.SLORules.MetadataRecRules.Rules, metaRules...) return nil } -func generateSLIRecordingRules(ctx context.Context, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { +func (p plugin) generateSLIRecordingRules(ctx context.Context, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { // Get the windows we need the recording rules. - windows := getAlertGroupWindows(alerts) + windows := alerts.TimeDurationWindows() windows = append(windows, slo.TimeWindow) // Add the total time window as a handy helper. // Generate the rules rules := make([]rulefmt.Rule, 0, len(windows)) for _, window := range windows { - rule, err := denominatorCorrectedSLIRecordGenerator(slo, window, alerts) + rule, err := p.denominatorCorrectedSLIRecordGenerator(slo, window, alerts) if err != nil { return nil, fmt.Errorf("could not create %q SLO rule for window %s: %w", slo.ID, window, err) } @@ -88,31 +84,22 @@ func generateSLIRecordingRules(ctx context.Context, slo model.PromSLO, alerts mo return rules, nil } -// getAlertGroupWindows gets all the time windows from a multiwindow multiburn alert group. -func getAlertGroupWindows(alerts model.MWMBAlertGroup) []time.Duration { - // Use a map to avoid duplicated windows. - windows := map[string]time.Duration{ - alerts.PageQuick.ShortWindow.String(): alerts.PageQuick.ShortWindow, - alerts.PageQuick.LongWindow.String(): alerts.PageQuick.LongWindow, - alerts.PageSlow.ShortWindow.String(): alerts.PageSlow.ShortWindow, - alerts.PageSlow.LongWindow.String(): alerts.PageSlow.LongWindow, - alerts.TicketQuick.ShortWindow.String(): alerts.TicketQuick.ShortWindow, - alerts.TicketQuick.LongWindow.String(): alerts.TicketQuick.LongWindow, - alerts.TicketSlow.ShortWindow.String(): alerts.TicketSlow.ShortWindow, - alerts.TicketSlow.LongWindow.String(): alerts.TicketSlow.LongWindow, - } - - res := make([]time.Duration, 0, len(windows)) - for _, w := range windows { - res = append(res, w) +func (p plugin) generateMetaRecordingRules(ctx context.Context, slo model.PromSLO, alerts model.MWMBAlertGroup) ([]rulefmt.Rule, error) { + metadataLabels := utilsdata.MergeLabels(conventions.GetSLOIDPromLabels(slo), slo.Labels) + rules := []rulefmt.Rule{} + for _, window := range alerts.TimeDurationWindows() { + rule, err := createNumeratorCorrection(slo, metadataLabels, window, slo.TimeWindow) + if err != nil { + return nil, fmt.Errorf("could not create numerator rule: %v", err) + } + rules = append(rules, *rule) } - sort.SliceStable(res, func(i, j int) bool { return res[i] < res[j] }) - - return res + return rules, nil } -func denominatorCorrectedSLIRecordGenerator(slo model.PromSLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { - const sliExprTplFmt = `( +func (p plugin) denominatorCorrectedSLIRecordGenerator(slo model.PromSLO, window time.Duration, alerts model.MWMBAlertGroup) (*rulefmt.Rule, error) { + const ( + sliExprTplFmt = `( {{.numeratorCorrectionMetric}}{{.window}}{{.filter}} * on() %s @@ -120,9 +107,34 @@ func denominatorCorrectedSLIRecordGenerator(slo model.PromSLO, window time.Durat / (%s) ` - - sliExprTpl := fmt.Sprintf(sliExprTplFmt, slo.SLI.Events.ErrorQuery, slo.SLI.Events.TotalQuery) - + sliExprTotalWindowTplFmt = `(%s) +/ +(%s) +` + // For more information about optimized SLI check `sloth.dev/core/sli_rules/v1` plugin. + sliExprTotalWindowOptimizedTplFmt = `sum_over_time(%s[{{.window}}]) +/ ignoring ({{.windowKey}}) +count_over_time(%s[{{.window}}]) +` + ) + + sliExprTpl := "" + switch { + // Last window (total window) when not optimized. + case window == slo.TimeWindow && p.cfg.DisableOptimized: + sliExprTpl = fmt.Sprintf(sliExprTotalWindowTplFmt, slo.SLI.Events.ErrorQuery, slo.SLI.Events.TotalQuery) + + // Last window (total window) when optimized. + case window == slo.TimeWindow && !p.cfg.DisableOptimized: + shortWindowSLIRec := conventions.GetSLIErrorMetric(alerts.PageQuick.ShortWindow) + filter := promutils.LabelsToPromFilter(conventions.GetSLOIDPromLabels(slo)) + metric := shortWindowSLIRec + filter + sliExprTpl = fmt.Sprintf(sliExprTotalWindowOptimizedTplFmt, metric, metric) + + // Regular SLI. + default: + sliExprTpl = fmt.Sprintf(sliExprTplFmt, slo.SLI.Events.ErrorQuery, slo.SLI.Events.TotalQuery) + } // Render with our templated data. tpl, err := template.New("sliExpr").Option("missingkey=error").Parse(sliExprTpl) if err != nil { diff --git a/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin_test.go b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin_test.go index 94f7732e..47ff1768 100644 --- a/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin_test.go +++ b/internal/plugin/slo/contrib/denominator_corrected_rules_v1/plugin_test.go @@ -1,6 +1,7 @@ package plugin_test import ( + "encoding/json" "testing" "time" @@ -56,11 +57,13 @@ func baseSLO() model.PromSLO { func TestProcessSLO(t *testing.T) { tests := map[string]struct { - req pluginslov1.Request - expRes pluginslov1.Result - expErr bool + req pluginslov1.Request + expRes pluginslov1.Result + expErr bool + pluginConfig plugin.PluginConfig }{ "Having and SLO with invalid expression should fail.": { + pluginConfig: plugin.PluginConfig{DisableOptimized: true}, req: pluginslov1.Request{ SLO: model.PromSLO{ ID: "test", @@ -83,6 +86,7 @@ func TestProcessSLO(t *testing.T) { }, "Having and wrong variable in the expression should fail.": { + pluginConfig: plugin.PluginConfig{DisableOptimized: true}, req: pluginslov1.Request{ SLO: model.PromSLO{ ID: "test", @@ -105,6 +109,7 @@ func TestProcessSLO(t *testing.T) { }, "Having an SLO with raw SLI, should fail.": { + pluginConfig: plugin.PluginConfig{DisableOptimized: true}, req: pluginslov1.Request{ SLO: model.PromSLO{ ID: "test", @@ -123,7 +128,8 @@ func TestProcessSLO(t *testing.T) { expErr: true, }, - "Having an SLO with SLI and its mwmb alerts should create the recording rules.": { + "Having an SLO with SLI and its mwmb alerts should create the recording rules (not optimized).": { + pluginConfig: plugin.PluginConfig{DisableOptimized: true}, req: pluginslov1.Request{ SLO: baseSLO(), MWMBAlertGroup: baseAlertGroup(), @@ -217,7 +223,7 @@ func TestProcessSLO(t *testing.T) { }, { Record: "slo:sli_error:ratio_rate30d", - Expr: "(\nslo:numerator_correction:ratio30d{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[30d]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[30d])))\n", + Expr: "(sum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[30d])))\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[30d])))\n", Labels: map[string]string{ "global01k1": "global01v1", "global02k1": "global02v1", @@ -306,9 +312,187 @@ func TestProcessSLO(t *testing.T) { "sloth_slo": "slo1", }, }, + }}, + }, + }, + }, + + "Having an SLO with SLI and its mwmb alerts should create the recording rules (optimized).": { + pluginConfig: plugin.PluginConfig{}, + req: pluginslov1.Request{ + SLO: baseSLO(), + MWMBAlertGroup: baseAlertGroup(), + }, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Expr: "(\nslo:numerator_correction:ratio5m{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[5m]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[5m])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "5m", + }, + }, + { + Record: "slo:sli_error:ratio_rate30m", + Expr: "(\nslo:numerator_correction:ratio30m{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[30m]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[30m])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "30m", + }, + }, + { + Record: "slo:sli_error:ratio_rate1h", + Expr: "(\nslo:numerator_correction:ratio1h{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[1h]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[1h])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "1h", + }, + }, { - Record: "slo:numerator_correction:ratio30d", - Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Record: "slo:sli_error:ratio_rate2h", + Expr: "(\nslo:numerator_correction:ratio2h{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[2h]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[2h])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "2h", + }, + }, + { + Record: "slo:sli_error:ratio_rate6h", + Expr: "(\nslo:numerator_correction:ratio6h{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[6h]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[6h])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "6h", + }, + }, + { + Record: "slo:sli_error:ratio_rate1d", + Expr: "(\nslo:numerator_correction:ratio1d{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[1d]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[1d])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "1d", + }, + }, + { + Record: "slo:sli_error:ratio_rate3d", + Expr: "(\nslo:numerator_correction:ratio3d{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}\n* on()\nsum(rate(http_request_duration_seconds_count{job=\"myservice\",code=~\"(5..|429)\"}[3d]))\n)\n/\n(sum(rate(http_request_duration_seconds_count{job=\"myservice\"}[3d])))\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "3d", + }, + }, + { + Record: "slo:sli_error:ratio_rate30d", + Expr: "sum_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}[30d])\n/ ignoring (sloth_window)\ncount_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"svc01-slo1\", sloth_service=\"svc01\", sloth_slo=\"slo1\"}[30d])\n", + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + "sloth_window": "30d", + }, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:numerator_correction:ratio5m", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[5m])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio30m", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[30m])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio1h", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[1h])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio2h", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[2h])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio6h", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[6h])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio1d", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[1d])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, + Labels: map[string]string{ + "global01k1": "global01v1", + "global02k1": "global02v1", + "sloth_id": "svc01-slo1", + "sloth_service": "svc01", + "sloth_slo": "slo1", + }, + }, + { + Record: "slo:numerator_correction:ratio3d", + Expr: `(sum(rate(http_request_duration_seconds_count{job="myservice"}[3d])))/(sum(rate(http_request_duration_seconds_count{job="myservice"}[30d])))`, Labels: map[string]string{ "global01k1": "global01v1", "global02k1": "global02v1", @@ -328,13 +512,18 @@ func TestProcessSLO(t *testing.T) { assert := assert.New(t) require := require.New(t) + cfgBytes, err := json.Marshal(test.pluginConfig) + require.NoError(err) + // Load plugin. - plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{}) + pluginInstance, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{ + PluginConfiguration: cfgBytes, + }) require.NoError(err) // Execute plugin. gotRes := pluginslov1.Result{} - err = plugin.ProcessSLO(t.Context(), &test.req, &gotRes) + err = pluginInstance.ProcessSLO(t.Context(), &test.req, &gotRes) // Check result. if test.expErr { @@ -348,7 +537,7 @@ func TestProcessSLO(t *testing.T) { func BenchmarkPluginYaegi(b *testing.B) { plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{ - PluginConfiguration: []byte("{}"), + PluginConfiguration: []byte(`{"disableOptimized": true}`), }) if err != nil { b.Fatal(err) diff --git a/internal/plugin/slo/core/sli_rules_v1/plugin.go b/internal/plugin/slo/core/sli_rules_v1/plugin.go index 64cb9527..0f0411e6 100644 --- a/internal/plugin/slo/core/sli_rules_v1/plugin.go +++ b/internal/plugin/slo/core/sli_rules_v1/plugin.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "sort" "text/template" "time" @@ -57,7 +56,7 @@ func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, re func generateSLIRecordingRules(ctx context.Context, slo model.PromSLO, alerts model.MWMBAlertGroup, genFunc sliRulesgenFunc) ([]rulefmt.Rule, error) { // Get the windows we need the recording rules. - windows := getAlertGroupWindows(alerts) + windows := alerts.TimeDurationWindows() windows = append(windows, slo.TimeWindow) // Add the total time window as a handy helper. // Generate the rules @@ -224,26 +223,3 @@ count_over_time({{.metric}}{{.filter}}[{{.window}}]) ), }, nil } - -// getAlertGroupWindows gets all the time windows from a multiwindow multiburn alert group. -func getAlertGroupWindows(alerts model.MWMBAlertGroup) []time.Duration { - // Use a map to avoid duplicated windows. - windows := map[string]time.Duration{ - alerts.PageQuick.ShortWindow.String(): alerts.PageQuick.ShortWindow, - alerts.PageQuick.LongWindow.String(): alerts.PageQuick.LongWindow, - alerts.PageSlow.ShortWindow.String(): alerts.PageSlow.ShortWindow, - alerts.PageSlow.LongWindow.String(): alerts.PageSlow.LongWindow, - alerts.TicketQuick.ShortWindow.String(): alerts.TicketQuick.ShortWindow, - alerts.TicketQuick.LongWindow.String(): alerts.TicketQuick.LongWindow, - alerts.TicketSlow.ShortWindow.String(): alerts.TicketSlow.ShortWindow, - alerts.TicketSlow.LongWindow.String(): alerts.TicketSlow.LongWindow, - } - - res := make([]time.Duration, 0, len(windows)) - for _, w := range windows { - res = append(res, w) - } - sort.SliceStable(res, func(i, j int) bool { return res[i] < res[j] }) - - return res -} diff --git a/pkg/common/model/alert.go b/pkg/common/model/alert.go index 900a22b4..b9231e79 100644 --- a/pkg/common/model/alert.go +++ b/pkg/common/model/alert.go @@ -1,6 +1,9 @@ package model -import "time" +import ( + "sort" + "time" +) // AlertSeverity is the type of alert. type AlertSeverity int @@ -44,3 +47,27 @@ type MWMBAlertGroup struct { TicketQuick MWMBAlert TicketSlow MWMBAlert } + +// TimeDurationWindows is a helper method to get the list of unique and sorted time durations +// windows of the alert group. +func (m MWMBAlertGroup) TimeDurationWindows() []time.Duration { + // Use a map to avoid duplicated windows. + windows := map[string]time.Duration{ + m.PageQuick.ShortWindow.String(): m.PageQuick.ShortWindow, + m.PageQuick.LongWindow.String(): m.PageQuick.LongWindow, + m.PageSlow.ShortWindow.String(): m.PageSlow.ShortWindow, + m.PageSlow.LongWindow.String(): m.PageSlow.LongWindow, + m.TicketQuick.ShortWindow.String(): m.TicketQuick.ShortWindow, + m.TicketQuick.LongWindow.String(): m.TicketQuick.LongWindow, + m.TicketSlow.ShortWindow.String(): m.TicketSlow.ShortWindow, + m.TicketSlow.LongWindow.String(): m.TicketSlow.LongWindow, + } + + res := make([]time.Duration, 0, len(windows)) + for _, w := range windows { + res = append(res, w) + } + sort.SliceStable(res, func(i, j int) bool { return res[i] < res[j] }) + + return res +} From e48fd3c447b463ca6499d814cdd39b75a91365c9 Mon Sep 17 00:00:00 2001 From: Will Bollock Date: Tue, 7 Oct 2025 09:34:33 -0400 Subject: [PATCH 090/173] chore: tiny markdown heading fix (#682) super tiny markdown heading fix so it renders the heading right --- .../plugin/slo/contrib/validate_victoria_metrics_v1/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md index 885af953..c74aba38 100644 --- a/internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md +++ b/internal/plugin/slo/contrib/validate_victoria_metrics_v1/README.md @@ -32,7 +32,7 @@ slo_plugins: - id: sloth.dev/core/alert_rules/v1 # Default set again. ``` -### Explicit usage as app plugin +### Explicit usage as app plugin Disable all default logic and set a new logic for all the Sloth app by setting the custom victoria metrics validator plugin and setting again default Sloth SLO generator plugins. From 57d84a94c1269397556b4479d5d040dbd20a53d0 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 11 Oct 2025 10:16:06 +0200 Subject: [PATCH 091/173] Update deps Signed-off-by: Xabier Larrakoetxea --- go.mod | 74 +++++++++++++++++++++++++++---------------------------- go.sum | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 37 deletions(-) diff --git a/go.mod b/go.mod index 324b3a6d..78e8bc97 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/alecthomas/kingpin/v2 v2.4.0 github.com/caarlos0/env/v11 v11.3.1 github.com/oklog/run v1.2.0 - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.85.1-0.20250905145128-3d04de8b9cba - github.com/prometheus-operator/prometheus-operator/pkg/client v0.85.1-0.20250905145128-3d04de8b9cba + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.0 + github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.0 github.com/prometheus/client_golang v1.23.2 - github.com/prometheus/common v0.66.1 + github.com/prometheus/common v0.67.1 github.com/prometheus/prometheus v0.306.0 github.com/sirupsen/logrus v1.9.3 github.com/slok/reload v0.2.0 @@ -19,14 +19,14 @@ require ( github.com/stretchr/testify v1.11.1 github.com/traefik/yaegi v0.16.1 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.34.0 - k8s.io/apimachinery v0.34.0 - k8s.io/client-go v0.34.0 + k8s.io/api v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 sigs.k8s.io/structured-merge-diff/v6 v6.3.0 ) require ( - github.com/VictoriaMetrics/metrics v1.35.3 // indirect + github.com/VictoriaMetrics/metrics v1.40.2 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -38,20 +38,20 @@ require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.22.0 // indirect - github.com/go-openapi/jsonreference v0.21.1 // indirect - github.com/go-openapi/swag v0.24.1 // indirect - github.com/go-openapi/swag/cmdutils v0.24.0 // indirect - github.com/go-openapi/swag/conv v0.24.0 // indirect - github.com/go-openapi/swag/fileutils v0.24.0 // indirect - github.com/go-openapi/swag/jsonname v0.24.0 // indirect - github.com/go-openapi/swag/jsonutils v0.24.0 // indirect - github.com/go-openapi/swag/loading v0.24.0 // indirect - github.com/go-openapi/swag/mangling v0.24.0 // indirect - github.com/go-openapi/swag/netutils v0.24.0 // indirect - github.com/go-openapi/swag/stringutils v0.24.0 // indirect - github.com/go-openapi/swag/typeutils v0.24.0 // indirect - github.com/go-openapi/swag/yamlutils v0.24.0 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.2 // indirect + github.com/go-openapi/swag v0.25.1 // indirect + github.com/go-openapi/swag/cmdutils v0.25.1 // indirect + github.com/go-openapi/swag/conv v0.25.1 // indirect + github.com/go-openapi/swag/fileutils v0.25.1 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/swag/jsonutils v0.25.1 // indirect + github.com/go-openapi/swag/loading v0.25.1 // indirect + github.com/go-openapi/swag/mangling v0.25.1 // indirect + github.com/go-openapi/swag/netutils v0.25.1 // indirect + github.com/go-openapi/swag/stringutils v0.25.1 // indirect + github.com/go-openapi/swag/typeutils v0.25.1 // indirect + github.com/go-openapi/swag/yamlutils v0.25.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -59,7 +59,7 @@ require ( github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -67,35 +67,35 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/objx v0.5.3 // indirect github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/histogram v1.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.uber.org/atomic v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/time v0.12.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.32.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.34.0 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611 // indirect - k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect - sigs.k8s.io/controller-runtime v0.22.0 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/controller-runtime v0.22.3 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/go.sum b/go.sum index 4160c6a2..cb367536 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/OpenSLO/oslo v0.12.0 h1:0zdxMgFE59TUKpe/L4+5ujgmJZW/kAuCOJbyqrTX4lc= github.com/OpenSLO/oslo v0.12.0/go.mod h1:6jyOTkqBCdkgqLXJ6WtJj7+o0rSexl6YZbWwnxpGwtU= github.com/VictoriaMetrics/metrics v1.35.3 h1:DrQBBAjTb24WFlGAV9dAQsPDmDRyqL63kZ1Yfc+SRkM= github.com/VictoriaMetrics/metrics v1.35.3/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= +github.com/VictoriaMetrics/metrics v1.40.2 h1:OVSjKcQEx6JAwGeu8/KQm9Su5qJ72TMEW4xYn5vw3Ac= +github.com/VictoriaMetrics/metrics v1.40.2/go.mod h1:XE4uudAAIRaJE614Tl5HMrtoEU6+GDZO4QTnNSsZRuA= github.com/VictoriaMetrics/metricsql v0.84.8 h1:5JXrvPJiYkYNqJVT7+hMZmpAwRHd3txBdlVIw4rJ1VM= github.com/VictoriaMetrics/metricsql v0.84.8/go.mod h1:d4EisFO6ONP/HIGDYTAtwrejJBBeKGQYiRl095bS4QQ= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= @@ -79,32 +81,60 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= +github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= +github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= +github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8= +github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo= github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= +github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY= +github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= +github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= +github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= +github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU= +github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M= github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= +github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= +github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= +github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= +github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= +github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= +github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4= +github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE= github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= +github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= +github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= +github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= +github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= +github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= +github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -148,6 +178,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 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= @@ -174,14 +206,20 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.85.1-0.20250905145128-3d04de8b9cba h1:7AXX1aIvTfVgIZ602DKHIukPGEbmA6URI16iLHsx1Rw= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.85.1-0.20250905145128-3d04de8b9cba/go.mod h1:k94oCBI0HYfPrYJQtmuDIVOVC+2Tv1n5sdwfq6QLX6E= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.0 h1:qHIsKfA2yDNx6Ch+B8sEMNy4sDq+uijCVZBscziNe+M= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.0/go.mod h1:nPk0OteXBkbT0CRCa2oZQL1jRLW6RJ2fuIijHypeJdk= github.com/prometheus-operator/prometheus-operator/pkg/client v0.85.1-0.20250905145128-3d04de8b9cba h1:Y/jQl7HVjQKrr8m7ntSjuGmMAoRd3KvBhwvhZkyAcTU= github.com/prometheus-operator/prometheus-operator/pkg/client v0.85.1-0.20250905145128-3d04de8b9cba/go.mod h1:oKQxfOEQRyUzsktM+6dxb6Ux5gNL461vSTnSz7Rx18Q= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.0 h1:g8thIxgUNLmxatcrawaMc8pOF8gWTRxdmbl0w+iUvpk= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.0/go.mod h1:XT8KCWs8rm55R82SaM7t4m4OOdEHlZVpRHmIkuTnvwI= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= +github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/prometheus/prometheus v0.306.0 h1:Q0Pvz/ZKS6vVWCa1VSgNyNJlEe8hxdRlKklFg7SRhNw= @@ -203,6 +241,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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= @@ -225,6 +265,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= @@ -239,6 +281,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -246,6 +290,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -256,33 +301,48 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= +golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= 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= @@ -295,6 +355,8 @@ google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= @@ -309,20 +371,35 @@ 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.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611 h1:o4oKOsvSymDkZRsMAPZU7bRdwL+lPOK5VS10Dr1D6eg= k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.22.0 h1:mTOfibb8Hxwpx3xEkR56i7xSjB+nH4hZG37SrlCY5e0= sigs.k8s.io/controller-runtime v0.22.0/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y= +sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= From b71ee9d11d27860d12c661a916dec8accb54f163 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Mon, 13 Oct 2025 20:30:15 +0200 Subject: [PATCH 092/173] Add app level SLO plugins support on validate cmd Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/validate.go | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/cmd/sloth/commands/validate.go b/cmd/sloth/commands/validate.go index 3a7d2de8..3d13fe04 100644 --- a/cmd/sloth/commands/validate.go +++ b/cmd/sloth/commands/validate.go @@ -18,13 +18,15 @@ import ( ) type validateCommand struct { - slosInput string - slosExcludeRegex string - slosIncludeRegex string - extraLabels map[string]string - pluginsPaths []string - sloPeriodWindowsPath string - sloPeriod string + slosInput string + slosExcludeRegex string + slosIncludeRegex string + extraLabels map[string]string + pluginsPaths []string + sloPeriodWindowsPath string + sloPeriod string + sloPlugins []string + disableDefaultSLOPlugins bool } // NewValidateCommand returns the validate command. @@ -38,6 +40,8 @@ func NewValidateCommand(app *kingpin.Application) Command { cmd.Flag("plugins-path", "The path to SLI and SLO plugins (can be repeated).").Short('p').StringsVar(&c.pluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) + cmd.Flag("slo-plugins", `SLO plugins chain declaration in JSON format '{"id": "foo","priority": 0,"config": "{}"}' (Can be repeated).`).Short('s').StringsVar(&c.sloPlugins) + cmd.Flag("disable-default-slo-plugins", `Disables the default SLO plugins, normally used along with custom SLO plugins to fully customize Sloth behavior`).BoolVar(&c.disableDefaultSLOPlugins) return c } @@ -86,6 +90,12 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { return err } + // Load SLO plugin declarations at CMD level. + cmdLevelSLOPlugins, err := mapCmdPluginToModel(ctx, v.sloPlugins) + if err != nil { + return fmt.Errorf("could not load slo plugin declarations: %w", err) + } + // Windows repository. var wfs fs.FS if v.sloPeriodWindowsPath != "" { @@ -124,10 +134,12 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { splittedSLOsData := splitYAML(slxData) gen := generator{ - logger: log.Noop, - windowsRepo: windowsRepo, - extraLabels: v.extraLabels, - sloPluginRepo: pluginsRepo, + logger: log.Noop, + windowsRepo: windowsRepo, + extraLabels: v.extraLabels, + sloPluginRepo: pluginsRepo, + extraSLOPlugins: cmdLevelSLOPlugins, + disableDefaultSLOPlugins: v.disableDefaultSLOPlugins, } // Prepare file validation result and start validation result for every SLO in the file. From cf54655841fe397a979c05c530c60a81fb908c10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 07:02:07 +0000 Subject: [PATCH 093/173] build(deps): bump github.com/prometheus-operator/prometheus-operator/pkg/client Bumps [github.com/prometheus-operator/prometheus-operator/pkg/client](https://github.com/prometheus-operator/prometheus-operator) from 0.86.0 to 0.86.1. - [Release notes](https://github.com/prometheus-operator/prometheus-operator/releases) - [Changelog](https://github.com/prometheus-operator/prometheus-operator/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus-operator/prometheus-operator/compare/v0.86.0...v0.86.1) --- updated-dependencies: - dependency-name: github.com/prometheus-operator/prometheus-operator/pkg/client dependency-version: 0.86.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 6 ++-- go.sum | 98 ++++++---------------------------------------------------- 2 files changed, 12 insertions(+), 92 deletions(-) diff --git a/go.mod b/go.mod index 78e8bc97..8fd274bd 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,8 @@ require ( github.com/alecthomas/kingpin/v2 v2.4.0 github.com/caarlos0/env/v11 v11.3.1 github.com/oklog/run v1.2.0 - github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.0 - github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.0 + github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.1 + github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.1 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/common v0.67.1 github.com/prometheus/prometheus v0.306.0 @@ -57,9 +57,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.9.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/go.sum b/go.sum index cb367536..f8aae4cc 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/OpenSLO/oslo v0.12.0 h1:0zdxMgFE59TUKpe/L4+5ujgmJZW/kAuCOJbyqrTX4lc= github.com/OpenSLO/oslo v0.12.0/go.mod h1:6jyOTkqBCdkgqLXJ6WtJj7+o0rSexl6YZbWwnxpGwtU= -github.com/VictoriaMetrics/metrics v1.35.3 h1:DrQBBAjTb24WFlGAV9dAQsPDmDRyqL63kZ1Yfc+SRkM= -github.com/VictoriaMetrics/metrics v1.35.3/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= github.com/VictoriaMetrics/metrics v1.40.2 h1:OVSjKcQEx6JAwGeu8/KQm9Su5qJ72TMEW4xYn5vw3Ac= github.com/VictoriaMetrics/metrics v1.40.2/go.mod h1:XE4uudAAIRaJE614Tl5HMrtoEU6+GDZO4QTnNSsZRuA= github.com/VictoriaMetrics/metricsql v0.84.8 h1:5JXrvPJiYkYNqJVT7+hMZmpAwRHd3txBdlVIw4rJ1VM= @@ -79,60 +77,34 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= -github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= -github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA= -github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8= github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU= github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ= -github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8= -github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A= github.com/go-openapi/swag v0.25.1 h1:6uwVsx+/OuvFVPqfQmOOPsqTcm5/GkBhNwLqIR916n8= github.com/go-openapi/swag v0.25.1/go.mod h1:bzONdGlT0fkStgGPd3bhZf1MnuPkf2YAys6h+jZipOo= -github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I= -github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8= github.com/go-openapi/swag/cmdutils v0.25.1 h1:nDke3nAFDArAa631aitksFGj2omusks88GF1VwdYqPY= github.com/go-openapi/swag/cmdutils v0.25.1/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik= -github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c= github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= -github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak= -github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90= github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU= github.com/go-openapi/swag/fileutils v0.25.1/go.mod h1:+NXtt5xNZZqmpIpjqcujqojGFek9/w55b3ecmOdtg8M= -github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= -github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q= github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= -github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts= -github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0= github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= -github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc= -github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= -github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk= -github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc= github.com/go-openapi/swag/mangling v0.25.1 h1:XzILnLzhZPZNtmxKaz/2xIGPQsBsvmCjrJOWGNz/ync= github.com/go-openapi/swag/mangling v0.25.1/go.mod h1:CdiMQ6pnfAgyQGSOIYnZkXvqhnnwOn997uXZMAd/7mQ= -github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w= -github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM= github.com/go-openapi/swag/netutils v0.25.1 h1:2wFLYahe40tDUHfKT1GRC4rfa5T1B4GWZ+msEFA4Fl4= github.com/go-openapi/swag/netutils v0.25.1/go.mod h1:CAkkvqnUJX8NV96tNhEQvKz8SQo2KF0f7LleiJwIeRE= -github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM= -github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w= github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= -github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw= -github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI= github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= -github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c= -github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -160,8 +132,6 @@ github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3 github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= -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/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -176,10 +146,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 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= @@ -204,20 +170,14 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjL 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-operator/prometheus-operator/pkg/apis/monitoring v0.85.1-0.20250905145128-3d04de8b9cba h1:7AXX1aIvTfVgIZ602DKHIukPGEbmA6URI16iLHsx1Rw= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.85.1-0.20250905145128-3d04de8b9cba/go.mod h1:k94oCBI0HYfPrYJQtmuDIVOVC+2Tv1n5sdwfq6QLX6E= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.0 h1:qHIsKfA2yDNx6Ch+B8sEMNy4sDq+uijCVZBscziNe+M= -github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.0/go.mod h1:nPk0OteXBkbT0CRCa2oZQL1jRLW6RJ2fuIijHypeJdk= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.85.1-0.20250905145128-3d04de8b9cba h1:Y/jQl7HVjQKrr8m7ntSjuGmMAoRd3KvBhwvhZkyAcTU= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.85.1-0.20250905145128-3d04de8b9cba/go.mod h1:oKQxfOEQRyUzsktM+6dxb6Ux5gNL461vSTnSz7Rx18Q= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.0 h1:g8thIxgUNLmxatcrawaMc8pOF8gWTRxdmbl0w+iUvpk= -github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.0/go.mod h1:XT8KCWs8rm55R82SaM7t4m4OOdEHlZVpRHmIkuTnvwI= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.1 h1:j/GvU9UxlK5nuUKOWYOY0LRqcfHZl1ffTOa46+00Cys= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.1/go.mod h1:nPk0OteXBkbT0CRCa2oZQL1jRLW6RJ2fuIijHypeJdk= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.1 h1:IqbpBVUahr2978vI6GkoCeaaugklgtq0M0FRJ5Ytdkk= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.1/go.mod h1:dquFiWkRGsMzjxckZAOFSsqqhUYQ9UZtwBeqaOsrWCk= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= @@ -226,8 +186,8 @@ github.com/prometheus/prometheus v0.306.0 h1:Q0Pvz/ZKS6vVWCa1VSgNyNJlEe8hxdRlKkl github.com/prometheus/prometheus v0.306.0/go.mod h1:7hMSGyZHt0dcmZ5r4kFPJ/vxPQU99N5/BGwSPDxeZrQ= github.com/prometheus/sigv4 v0.2.0 h1:qDFKnHYFswJxdzGeRP63c4HlH3Vbn1Yf/Ao2zabtVXk= github.com/prometheus/sigv4 v0.2.0/go.mod h1:D04rqmAaPPEUkjRQxGqjoxdyJuyCh6E0M18fZr0zBiE= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slok/reload v0.2.0 h1:8ezO7EsaYMUCX2g1ZAdHTxD0Mo9oDn2legZZBoFMUEI= @@ -239,7 +199,6 @@ github.com/spotahome/kooper/v2 v2.9.0/go.mod h1:im2PUUOGti/fXq0tUZaowG6cWJvgzS9B 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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= @@ -263,8 +222,6 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8 github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= @@ -279,8 +236,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -288,9 +243,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -299,50 +253,35 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn 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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= 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= @@ -353,8 +292,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -369,35 +306,20 @@ 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.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= -k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= -k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= -k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= -k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= -k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= -k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611 h1:o4oKOsvSymDkZRsMAPZU7bRdwL+lPOK5VS10Dr1D6eg= -k8s.io/kube-openapi v0.0.0-20250905212525-66792eed8611/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0= -k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.22.0 h1:mTOfibb8Hxwpx3xEkR56i7xSjB+nH4hZG37SrlCY5e0= -sigs.k8s.io/controller-runtime v0.22.0/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y= sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= From 2b4c01fafc1e3155be404cb588923735cb20d637 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Tue, 14 Oct 2025 19:07:27 +0200 Subject: [PATCH 094/173] Bump v0.14.0 Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 12 +++++++++++- .../tests/testdata/output/deployment_custom.yaml | 2 +- .../tests/testdata/output/deployment_default.yaml | 4 ++-- deploy/kubernetes/helm/sloth/values.yaml | 4 ++-- deploy/kubernetes/raw/sloth-with-common-plugins.yaml | 4 ++-- deploy/kubernetes/raw/sloth.yaml | 2 +- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b23430bb..f8232761 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [v0.14.0] - 2025-10-13 + ### Added - Add contrib plugin directory and CODEOWNERS policies. @@ -11,6 +13,13 @@ - Contrib plugin: `sloth.dev/contrib/rule_intervals/v1`. - Contrib plugin: `sloth.dev/contrib/error_budget_exhausted_alert/v1`. - Contrib plugin: `sloth.dev/contrib/denominator_corrected_rules/v1`. +- Add `--slo-plugins` and `-s` flag (`validate`) to be able to declare SLO plugins at cmd level, these plugins will be applied to all SLOs. +- Add `--disable-default-slo-plugins` flag (`validate`) to be able to disable default Sloth SLO plugins. + + +### Changed + +- Update chat git sync to v4.5.0 ## [v0.13.0] - 2025-09-10 @@ -221,7 +230,8 @@ - Support raw query based SLI. - Kubernetes (prometheus-operator) CRD generation support. -[unreleased]: https://github.com/slok/sloth/compare/v0.13.0...HEAD +[unreleased]: https://github.com/slok/sloth/compare/v0.14.0...HEAD +[v0.14.0]: https://github.com/slok/sloth/compare/v0.13.0...v0.14.0 [v0.13.0]: https://github.com/slok/sloth/compare/v0.12.0...v0.13.0 [v0.12.0]: https://github.com/slok/sloth/compare/v0.11.0...v0.12.0 [v0.11.0]: https://github.com/slok/sloth/compare/v0.10.0...v0.11.0 diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml index a32c5299..d44c77de 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_custom.yaml @@ -70,7 +70,7 @@ spec: cpu: 5m memory: 75Mi - name: git-sync-plugins - image: registry.k8s.io/git-sync/git-sync:v4.4.0 + image: registry.k8s.io/git-sync/git-sync:v4.5.0 args: - --repo=https://github.com/slok/sloth-test-common-sli-plugins - --ref=main diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml index 2c8bad81..4dfdb814 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml @@ -32,7 +32,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.13.0 + image: ghcr.io/slok/sloth:v0.14.0 args: - kubernetes-controller - --plugins-path=/plugins @@ -52,7 +52,7 @@ spec: cpu: 5m memory: 75Mi - name: git-sync-plugins - image: registry.k8s.io/git-sync/git-sync:v4.4.0 + image: registry.k8s.io/git-sync/git-sync:v4.5.0 args: - --repo=https://github.com/slok/sloth-common-sli-plugins - --ref=main diff --git a/deploy/kubernetes/helm/sloth/values.yaml b/deploy/kubernetes/helm/sloth/values.yaml index 6557adff..08ff3794 100644 --- a/deploy/kubernetes/helm/sloth/values.yaml +++ b/deploy/kubernetes/helm/sloth/values.yaml @@ -6,7 +6,7 @@ labels: {} image: registry: ghcr.io # This field cannot be empty if global.imageRegistry is also empty. repository: slok/sloth - tag: v0.13.0 + tag: v0.14.0 # -- Container resources: requests and limits for CPU, Memory resources: @@ -37,7 +37,7 @@ commonPlugins: enabled: true image: repository: registry.k8s.io/git-sync/git-sync - tag: v4.4.0 + tag: v4.5.0 gitRepo: url: https://github.com/slok/sloth-common-sli-plugins branch: main diff --git a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml index 48590f5b..1f0da79f 100644 --- a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml +++ b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml @@ -85,7 +85,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.13.0 + image: ghcr.io/slok/sloth:v0.14.0 args: - kubernetes-controller - --plugins-path=/plugins @@ -105,7 +105,7 @@ spec: cpu: 5m memory: 75Mi - name: git-sync-plugins - image: registry.k8s.io/git-sync/git-sync:v4.4.0 + image: registry.k8s.io/git-sync/git-sync:v4.5.0 args: - --repo=https://github.com/slok/sloth-common-sli-plugins - --ref=main diff --git a/deploy/kubernetes/raw/sloth.yaml b/deploy/kubernetes/raw/sloth.yaml index 13c3c06f..67206bcf 100644 --- a/deploy/kubernetes/raw/sloth.yaml +++ b/deploy/kubernetes/raw/sloth.yaml @@ -85,7 +85,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.13.0 + image: ghcr.io/slok/sloth:v0.14.0 args: - kubernetes-controller - --logger=default From 2da02ee4676c5044a5c49b1b57bc0b318b28ee38 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Tue, 14 Oct 2025 20:47:59 +0200 Subject: [PATCH 095/173] Bump helm chart to v0.14 Signed-off-by: Xabier Larrakoetxea --- deploy/kubernetes/helm/sloth/Chart.yaml | 2 +- deploy/kubernetes/raw/sloth-with-common-plugins.yaml | 12 ++++++------ deploy/kubernetes/raw/sloth.yaml | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/deploy/kubernetes/helm/sloth/Chart.yaml b/deploy/kubernetes/helm/sloth/Chart.yaml index 5979b073..bd829209 100644 --- a/deploy/kubernetes/helm/sloth/Chart.yaml +++ b/deploy/kubernetes/helm/sloth/Chart.yaml @@ -4,4 +4,4 @@ description: Base chart for Sloth. type: application home: https://github.com/slok/sloth kubeVersion: ">= 1.19.0-0" -version: 0.13.0 +version: 0.14.0 diff --git a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml index 1f0da79f..61886c07 100644 --- a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml +++ b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -133,7 +133,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth diff --git a/deploy/kubernetes/raw/sloth.yaml b/deploy/kubernetes/raw/sloth.yaml index 67206bcf..09e7a1a8 100644 --- a/deploy/kubernetes/raw/sloth.yaml +++ b/deploy/kubernetes/raw/sloth.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -108,7 +108,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.13.0 + helm.sh/chart: sloth-0.14.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth From 2019bc623ebef0d17e65683a136f428a4aa6bdbf Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 18 Oct 2025 10:59:12 +0200 Subject: [PATCH 096/173] Add Sloth library Godoc examples Signed-off-by: Xabier Larrakoetxea --- pkg/lib/lib_as_cli_use_cases_test.go | 265 ++++++++++++++++++ pkg/lib/lib_test.go | 400 +++++++++++---------------- pkg/lib/pkg_example_test.go | 54 ++++ 3 files changed, 484 insertions(+), 235 deletions(-) create mode 100644 pkg/lib/lib_as_cli_use_cases_test.go create mode 100644 pkg/lib/pkg_example_test.go diff --git a/pkg/lib/lib_as_cli_use_cases_test.go b/pkg/lib/lib_as_cli_use_cases_test.go new file mode 100644 index 00000000..4daf4951 --- /dev/null +++ b/pkg/lib/lib_as_cli_use_cases_test.go @@ -0,0 +1,265 @@ +package lib_test + +import ( + "bytes" + "io/fs" + "os" + "testing" + "text/template" + "time" + + "github.com/slok/sloth/internal/info" + "github.com/slok/sloth/pkg/common/model" + "github.com/slok/sloth/pkg/lib" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// These tests try being as real as possible by using the library to tests against the CLI integration tests. +// Regarding the features, the CLI offers file and IO operations that the library doesn't offer. However the +// core SLO generator logic is the same (the CLI uses the public library under the hood). +func TestLibAsCLIIntegration(t *testing.T) { + testWindowsFS := os.DirFS("../../test/integration/prometheus/windows") + testPluginsFS := os.DirFS("../../test/integration/prometheus/plugins") + + tests := map[string]struct { + config func() lib.Config + inFilePath string + resultFormatter func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte + expOutFilePath string + expGenErr bool + }{ + "Invalid spec case.": { + config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + inFilePath: "../../test/integration/prometheus/testdata/in-invalid-version.yaml", + expGenErr: true, + }, + + "Prometheus case.": { + config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", + expOutFilePath: "../../test/integration/prometheus/testdata/out-base.yaml.tpl", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + var b bytes.Buffer + err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + require.NoError(t, err) + return b.Bytes() + }, + }, + + "Kubernetes case.": { + config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + inFilePath: "../../test/integration/prometheus/testdata/in-base-k8s.yaml", + expOutFilePath: "../../test/integration/prometheus/testdata/out-base-k8s.yaml.tpl", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + var b bytes.Buffer + kmeta := lib.K8sMeta{Name: "svc", Namespace: "test-ns"} + err := lib.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) + require.NoError(t, err) + return b.Bytes() + }, + }, + + "OpenSLO case.": { + config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + inFilePath: "../../test/integration/prometheus/testdata/in-openslo.yaml", + expOutFilePath: "../../test/integration/prometheus/testdata/out-openslo.yaml.tpl", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + var b bytes.Buffer + err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + require.NoError(t, err) + return b.Bytes() + }, + }, + + "Default 28d window period case.": { + config: func() lib.Config { + return lib.Config{ + DefaultSLOPeriod: 28 * 24 * time.Hour, + CallerAgent: lib.CallerAgentCLI, + } + }, + inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", + expOutFilePath: "../../test/integration/prometheus/testdata/out-base-28d.yaml.tpl", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + var b bytes.Buffer + err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + require.NoError(t, err) + return b.Bytes() + }, + }, + + "Custom 7d window period case.": { + config: func() lib.Config { + return lib.Config{ + DefaultSLOPeriod: 7 * 24 * time.Hour, + CallerAgent: lib.CallerAgentCLI, + WindowsFS: testWindowsFS, + } + }, + inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", + expOutFilePath: "../../test/integration/prometheus/testdata/out-base-custom-windows-7d.yaml.tpl", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + var b bytes.Buffer + err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + require.NoError(t, err) + return b.Bytes() + }, + }, + + "Extra labels case.": { + config: func() lib.Config { + return lib.Config{ + CallerAgent: lib.CallerAgentCLI, + ExtraLabels: map[string]string{"exk1": "exv1", "exk2": "exv2"}, + } + }, + inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", + expOutFilePath: "../../test/integration/prometheus/testdata/out-base-extra-labels.yaml.tpl", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + var b bytes.Buffer + err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + require.NoError(t, err) + return b.Bytes() + }, + }, + + "No alerts case.": { + config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", + expOutFilePath: "../../test/integration/prometheus/testdata/out-base-no-alerts.yaml.tpl", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + + for i := range result.SLOResult { + result.SLOResult[i].PrometheusRules.AlertRules = model.PromRuleGroup{} + } + + var b bytes.Buffer + err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + require.NoError(t, err) + return b.Bytes() + }, + }, + + "No recording rules case.": { + config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", + expOutFilePath: "../../test/integration/prometheus/testdata/out-base-no-recordings.yaml.tpl", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + // Remove alerts. + for i := range result.SLOResult { + result.SLOResult[i].PrometheusRules.SLIErrorRecRules = model.PromRuleGroup{} + result.SLOResult[i].PrometheusRules.MetadataRecRules = model.PromRuleGroup{} + } + + var b bytes.Buffer + err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + require.NoError(t, err) + return b.Bytes() + }, + }, + + "SLI plugin usage.": { + config: func() lib.Config { + return lib.Config{ + CallerAgent: lib.CallerAgentCLI, + PluginsFS: []fs.FS{testPluginsFS}, + } + }, + inFilePath: "../../test/integration/prometheus/testdata/in-sli-plugin.yaml", + expOutFilePath: "../../test/integration/prometheus/testdata/out-sli-plugin.yaml.tpl", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + var b bytes.Buffer + err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + require.NoError(t, err) + return b.Bytes() + }, + }, + + "SLO plugin usage.": { + config: func() lib.Config { + return lib.Config{ + CallerAgent: lib.CallerAgentCLI, + PluginsFS: []fs.FS{testPluginsFS}, + } + }, + inFilePath: "../../test/integration/prometheus/testdata/in-slo-plugin.yaml", + expOutFilePath: "../../test/integration/prometheus/testdata/out-slo-plugin.yaml.tpl", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + var b bytes.Buffer + err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + require.NoError(t, err) + return b.Bytes() + }, + }, + + "SLO plugin K8s usage.": { + config: func() lib.Config { + return lib.Config{ + CallerAgent: lib.CallerAgentCLI, + PluginsFS: []fs.FS{testPluginsFS}, + } + }, + inFilePath: "../../test/integration/prometheus/testdata/in-slo-plugin-k8s.yaml", + expOutFilePath: "../../test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + var b bytes.Buffer + kmeta := lib.K8sMeta{Name: "svc", Namespace: "test-ns"} + err := lib.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) + require.NoError(t, err) + return b.Bytes() + }, + }, + + "A multifile case (Not supported).": { + config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + inFilePath: "../../test/integration/prometheus/testdata/in-multifile.yaml", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { return nil }, + expGenErr: true, + }, + + "A multifile Kubernetes case (Not supported).": { + config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + inFilePath: "../../test/integration/prometheus/testdata/in-multifile-k8s.yaml", + resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { return nil }, + expGenErr: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + gen, err := lib.NewPrometheusSLOGenerator(test.config()) + require.NoError(err) + + // Generate. + expInData, err := os.ReadFile(test.inFilePath) + require.NoError(err) + result, err := gen.GenerateFromRaw(t.Context(), expInData) + if test.expGenErr { + assert.Error(err) + return + } else if assert.NoError(err) { + // Check result. + resultOutData := test.resultFormatter(t, *result) + expOutData := getExpData(t, test.expOutFilePath) + assert.Equal(string(expOutData), string(resultOutData)) + } + }) + } +} + +func getExpData(t *testing.T, path string) []byte { + expOutData, err := os.ReadFile(path) + require.NoError(t, err) + + var b bytes.Buffer + err = template.Must(template.New("").Parse(string(expOutData))).Execute(&b, map[string]string{ + "version": info.Version, + }) + require.NoError(t, err) + + return b.Bytes() +} diff --git a/pkg/lib/lib_test.go b/pkg/lib/lib_test.go index 4daf4951..e2be71c3 100644 --- a/pkg/lib/lib_test.go +++ b/pkg/lib/lib_test.go @@ -1,265 +1,195 @@ package lib_test import ( - "bytes" - "io/fs" + "context" "os" - "testing" - "text/template" - "time" - "github.com/slok/sloth/internal/info" - "github.com/slok/sloth/pkg/common/model" + slotk8sv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" "github.com/slok/sloth/pkg/lib" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + slothprometheusv1 "github.com/slok/sloth/pkg/prometheus/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// These tests try being as real as possible by using the library to tests against the CLI integration tests. -// Regarding the features, the CLI offers file and IO operations that the library doesn't offer. However the -// core SLO generator logic is the same (the CLI uses the public library under the hood). -func TestLibAsCLIIntegration(t *testing.T) { - testWindowsFS := os.DirFS("../../test/integration/prometheus/windows") - testPluginsFS := os.DirFS("../../test/integration/prometheus/plugins") - - tests := map[string]struct { - config func() lib.Config - inFilePath string - resultFormatter func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte - expOutFilePath string - expGenErr bool - }{ - "Invalid spec case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, - inFilePath: "../../test/integration/prometheus/testdata/in-invalid-version.yaml", - expGenErr: true, - }, - - "Prometheus case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, - inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", - expOutFilePath: "../../test/integration/prometheus/testdata/out-base.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { - var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) - require.NoError(t, err) - return b.Bytes() - }, - }, - - "Kubernetes case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, - inFilePath: "../../test/integration/prometheus/testdata/in-base-k8s.yaml", - expOutFilePath: "../../test/integration/prometheus/testdata/out-base-k8s.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { - var b bytes.Buffer - kmeta := lib.K8sMeta{Name: "svc", Namespace: "test-ns"} - err := lib.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) - require.NoError(t, err) - return b.Bytes() - }, - }, +func ExamplePrometheusSLOGenerator_GenerateFromRaw() { + sloSpec := []byte(` +--- +version: "prometheus/v1" +service: "myservice" +labels: + owner: "myteam" + repo: "myorg/myservice" + tier: "2" +slos: + # We allow failing (5xx and 429) 1 request every 1000 requests (99.9%). + - name: "requests-availability" + objective: 99.9 + description: "Common SLO based on availability for HTTP request responses." + labels: + category: availability + sli: + events: + error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) + total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) + alerting: + name: "MyServiceHighErrorRate" + labels: + category: "availability" + annotations: + # Overwrite default Sloth SLO alert summmary on ticket and page alerts. + summary: "High error rate on 'myservice' requests responses" + page_alert: + labels: + severity: "pageteam" + routing_key: "myteam" + ticket_alert: + labels: + severity: "slack" + slack_channel: "#alerts-myteam" +`) + + ctx := context.Background() + + gen, err := lib.NewPrometheusSLOGenerator(lib.Config{ + ExtraLabels: map[string]string{"source": "slothlib-example"}, + }) + if err != nil { + panic(err) + } - "OpenSLO case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, - inFilePath: "../../test/integration/prometheus/testdata/in-openslo.yaml", - expOutFilePath: "../../test/integration/prometheus/testdata/out-openslo.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { - var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) - require.NoError(t, err) - return b.Bytes() - }, - }, + // Generate SLO and write result. + slo, err := gen.GenerateFromRaw(ctx, sloSpec) + if err != nil { + panic(err) + } - "Default 28d window period case.": { - config: func() lib.Config { - return lib.Config{ - DefaultSLOPeriod: 28 * 24 * time.Hour, - CallerAgent: lib.CallerAgentCLI, - } - }, - inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", - expOutFilePath: "../../test/integration/prometheus/testdata/out-base-28d.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { - var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) - require.NoError(t, err) - return b.Bytes() - }, - }, + err = lib.WriteResultAsPrometheusStd(ctx, *slo, os.Stdout) + if err != nil { + panic(err) + } +} - "Custom 7d window period case.": { - config: func() lib.Config { - return lib.Config{ - DefaultSLOPeriod: 7 * 24 * time.Hour, - CallerAgent: lib.CallerAgentCLI, - WindowsFS: testWindowsFS, - } - }, - inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", - expOutFilePath: "../../test/integration/prometheus/testdata/out-base-custom-windows-7d.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { - var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) - require.NoError(t, err) - return b.Bytes() - }, +func ExamplePrometheusSLOGenerator_GenerateFromSlothV1() { + sloSpec := slothprometheusv1.Spec{ + Service: "myservice", + Labels: map[string]string{ + "owner": "myteam", + "repo": "myorg/myservice", + "tier": "2", }, - - "Extra labels case.": { - config: func() lib.Config { - return lib.Config{ - CallerAgent: lib.CallerAgentCLI, - ExtraLabels: map[string]string{"exk1": "exv1", "exk2": "exv2"}, - } - }, - inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", - expOutFilePath: "../../test/integration/prometheus/testdata/out-base-extra-labels.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { - var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) - require.NoError(t, err) - return b.Bytes() + SLOs: []slothprometheusv1.SLO{ + { + Name: "requests-availability", + Objective: 99.9, + Description: "Common SLO based on availability for HTTP request responses.", + SLI: slothprometheusv1.SLI{ + Events: &slothprometheusv1.SLIEvents{ + ErrorQuery: `sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}]))`, + TotalQuery: `sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}]))`, + }, + }, + Alerting: slothprometheusv1.Alerting{ + Name: "MyServiceHighErrorRate", + Labels: map[string]string{"category": "availability"}, + Annotations: map[string]string{"summary": "High error rate on 'myservice' requests responses"}, + PageAlert: slothprometheusv1.Alert{ + Labels: map[string]string{ + "severity": "page", + "routing_key": "myteam", + }, + }, + TicketAlert: slothprometheusv1.Alert{ + Labels: map[string]string{ + "severity": "slack", + "slack_channel": "#alerts-myteam", + }, + }, + }, }, }, + } - "No alerts case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, - inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", - expOutFilePath: "../../test/integration/prometheus/testdata/out-base-no-alerts.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { - - for i := range result.SLOResult { - result.SLOResult[i].PrometheusRules.AlertRules = model.PromRuleGroup{} - } - - var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) - require.NoError(t, err) - return b.Bytes() - }, - }, + ctx := context.Background() - "No recording rules case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, - inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", - expOutFilePath: "../../test/integration/prometheus/testdata/out-base-no-recordings.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { - // Remove alerts. - for i := range result.SLOResult { - result.SLOResult[i].PrometheusRules.SLIErrorRecRules = model.PromRuleGroup{} - result.SLOResult[i].PrometheusRules.MetadataRecRules = model.PromRuleGroup{} - } + gen, err := lib.NewPrometheusSLOGenerator(lib.Config{ + ExtraLabels: map[string]string{"source": "slothlib-example"}, + }) + if err != nil { + panic(err) + } - var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) - require.NoError(t, err) - return b.Bytes() - }, - }, + // Generate SLO and write result. + slo, err := gen.GenerateFromSlothV1(ctx, sloSpec) + if err != nil { + panic(err) + } - "SLI plugin usage.": { - config: func() lib.Config { - return lib.Config{ - CallerAgent: lib.CallerAgentCLI, - PluginsFS: []fs.FS{testPluginsFS}, - } - }, - inFilePath: "../../test/integration/prometheus/testdata/in-sli-plugin.yaml", - expOutFilePath: "../../test/integration/prometheus/testdata/out-sli-plugin.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { - var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) - require.NoError(t, err) - return b.Bytes() - }, - }, + err = lib.WriteResultAsPrometheusStd(ctx, *slo, os.Stdout) + if err != nil { + panic(err) + } +} - "SLO plugin usage.": { - config: func() lib.Config { - return lib.Config{ - CallerAgent: lib.CallerAgentCLI, - PluginsFS: []fs.FS{testPluginsFS}, - } - }, - inFilePath: "../../test/integration/prometheus/testdata/in-slo-plugin.yaml", - expOutFilePath: "../../test/integration/prometheus/testdata/out-slo-plugin.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { - var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) - require.NoError(t, err) - return b.Bytes() +func ExamplePrometheusSLOGenerator_GenerateFromK8sV1() { + sloSpec := slotk8sv1.PrometheusServiceLevel{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test01", + Labels: map[string]string{ + "prometheus": "default", }, }, - - "SLO plugin K8s usage.": { - config: func() lib.Config { - return lib.Config{ - CallerAgent: lib.CallerAgentCLI, - PluginsFS: []fs.FS{testPluginsFS}, - } + Spec: slotk8sv1.PrometheusServiceLevelSpec{ + Service: "svc01", + Labels: map[string]string{ + "globalk1": "globalv1", + }, + SLOs: []slotk8sv1.SLO{ + { + Name: "slo01", + Objective: 99.9, + Labels: map[string]string{ + "slo01k1": "slo01v1", + }, + SLI: slotk8sv1.SLI{Events: &slotk8sv1.SLIEvents{ + ErrorQuery: `sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}]))`, + TotalQuery: `sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}]))`, + }}, + Alerting: slotk8sv1.Alerting{ + Name: "myServiceAlert", + Labels: map[string]string{ + "alert01k1": "alert01v1", + }, + Annotations: map[string]string{ + "alert02k1": "alert02v1", + }, + PageAlert: slotk8sv1.Alert{}, + TicketAlert: slotk8sv1.Alert{}, + }, + }, }, - inFilePath: "../../test/integration/prometheus/testdata/in-slo-plugin-k8s.yaml", - expOutFilePath: "../../test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { - var b bytes.Buffer - kmeta := lib.K8sMeta{Name: "svc", Namespace: "test-ns"} - err := lib.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) - require.NoError(t, err) - return b.Bytes() - }, - }, - - "A multifile case (Not supported).": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, - inFilePath: "../../test/integration/prometheus/testdata/in-multifile.yaml", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { return nil }, - expGenErr: true, - }, - - "A multifile Kubernetes case (Not supported).": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, - inFilePath: "../../test/integration/prometheus/testdata/in-multifile-k8s.yaml", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { return nil }, - expGenErr: true, }, } - for name, test := range tests { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - gen, err := lib.NewPrometheusSLOGenerator(test.config()) - require.NoError(err) + ctx := context.Background() - // Generate. - expInData, err := os.ReadFile(test.inFilePath) - require.NoError(err) - result, err := gen.GenerateFromRaw(t.Context(), expInData) - if test.expGenErr { - assert.Error(err) - return - } else if assert.NoError(err) { - // Check result. - resultOutData := test.resultFormatter(t, *result) - expOutData := getExpData(t, test.expOutFilePath) - assert.Equal(string(expOutData), string(resultOutData)) - } - }) + gen, err := lib.NewPrometheusSLOGenerator(lib.Config{ + ExtraLabels: map[string]string{"source": "slothlib-example"}, + }) + if err != nil { + panic(err) } -} - -func getExpData(t *testing.T, path string) []byte { - expOutData, err := os.ReadFile(path) - require.NoError(t, err) - var b bytes.Buffer - err = template.Must(template.New("").Parse(string(expOutData))).Execute(&b, map[string]string{ - "version": info.Version, - }) - require.NoError(t, err) + // Generate SLO and write result. + slo, err := gen.GenerateFromK8sV1(ctx, sloSpec) + if err != nil { + panic(err) + } - return b.Bytes() + kmeta := lib.K8sMeta{ + Name: "sloth-slo-gen-" + sloSpec.ObjectMeta.Name, + Namespace: sloSpec.ObjectMeta.Namespace, + } + err = lib.WriteResultAsK8sPrometheusOperator(ctx, kmeta, *slo, os.Stdout) + if err != nil { + panic(err) + } } diff --git a/pkg/lib/pkg_example_test.go b/pkg/lib/pkg_example_test.go new file mode 100644 index 00000000..3c0aacc2 --- /dev/null +++ b/pkg/lib/pkg_example_test.go @@ -0,0 +1,54 @@ +package lib_test + +import ( + "fmt" + "io" + "net/http" + + sloth "github.com/slok/sloth/pkg/lib" +) + +// This example shows a basic usage of sloth library by exposing sloth SLO generation functionality as a rest HTTP API. +func Example() { + // Check with `curl -XPOST http://127.0.0.1:8080/sloth/generate -d "$(cat ./examples/getting-started.yml)"`. + + gen, err := sloth.NewPrometheusSLOGenerator(sloth.Config{ + ExtraLabels: map[string]string{"source": "slothlib-example"}, + }) + if err != nil { + panic(fmt.Errorf("could not create SLO generator: %w", err)) + } + + mux := http.NewServeMux() + mux.HandleFunc("POST /sloth/generate", func(w http.ResponseWriter, r *http.Request) { + // Get body from request. + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "failed to read request body", http.StatusBadRequest) + return + } + defer r.Body.Close() + + // Generate SLOs. + result, err := gen.GenerateFromRaw(r.Context(), body) + if err != nil { + http.Error(w, fmt.Sprintf("could not generate SLOs: %v", err), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + err = sloth.WriteResultAsPrometheusStd(r.Context(), *result, w) + if err != nil { + http.Error(w, fmt.Sprintf("could not write result: %v", err), http.StatusInternalServerError) + return + } + }) + + httpServer := &http.Server{Addr: ":8080", Handler: mux} + + fmt.Println("Starting server at :8080") + + err = httpServer.ListenAndServe() + if err != nil { + panic(err) + } +} From a7a4c8c7e6303fc136c86a76a4f9c61cdab46105 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sun, 19 Oct 2025 19:04:29 +0200 Subject: [PATCH 097/173] Fix naming in sloth lib slo generator config Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 2 +- cmd/sloth/commands/validate.go | 2 +- .../github_com-slok-sloth-pkg-common-model.go | 3 + ...ub_com-slok-sloth-pkg-common-utils-data.go | 1 + pkg/lib/bench_test.go | 68 +++++++++++++++++++ pkg/lib/gen.go | 8 +-- pkg/lib/lib_as_cli_use_cases_test.go | 58 ++++++++++------ pkg/lib/lib_test.go | 6 +- pkg/lib/pkg_example_test.go | 2 +- 9 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 pkg/lib/bench_test.go diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index c5c6f02e..d91f72b4 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -223,7 +223,7 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { wfs = os.DirFS(g.sloPeriodWindowsPath) } - genService, err := slothlib.NewPrometheusSLOGenerator(slothlib.Config{ + genService, err := slothlib.NewPrometheusSLOGenerator(slothlib.PrometheusSLOGeneratorConfig{ WindowsFS: wfs, PluginsFS: pluginsFSs, DefaultSLOPeriod: sloPeriod, diff --git a/cmd/sloth/commands/validate.go b/cmd/sloth/commands/validate.go index 15ad33fc..51317947 100644 --- a/cmd/sloth/commands/validate.go +++ b/cmd/sloth/commands/validate.go @@ -101,7 +101,7 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { wfs = os.DirFS(v.sloPeriodWindowsPath) } - genService, err := slothlib.NewPrometheusSLOGenerator(slothlib.Config{ + genService, err := slothlib.NewPrometheusSLOGenerator(slothlib.PrometheusSLOGeneratorConfig{ WindowsFS: wfs, PluginsFS: pluginsFSs, DefaultSLOPeriod: sloPeriod, diff --git a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go index fe867ecc..8df09429 100644 --- a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go +++ b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go @@ -12,6 +12,9 @@ import ( func init() { Symbols["github.com/slok/sloth/pkg/common/model/model"] = map[string]reflect.Value{ // function, constant and variable definitions + "ModeAPIGenKubernetes": reflect.ValueOf(constant.MakeFromLiteral("\"api-gen-k8s\"", token.STRING, 0)), + "ModeAPIGenOpenSLO": reflect.ValueOf(constant.MakeFromLiteral("\"api-gen-openslo\"", token.STRING, 0)), + "ModeAPIGenPrometheus": reflect.ValueOf(constant.MakeFromLiteral("\"api-gen-prom\"", token.STRING, 0)), "ModeCLIGenKubernetes": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-k8s\"", token.STRING, 0)), "ModeCLIGenOpenSLO": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-openslo\"", token.STRING, 0)), "ModeCLIGenPrometheus": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-prom\"", token.STRING, 0)), diff --git a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go index b8f1a60f..09751b38 100644 --- a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go +++ b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-utils-data.go @@ -11,5 +11,6 @@ func init() { Symbols["github.com/slok/sloth/pkg/common/utils/data/data"] = map[string]reflect.Value{ // function, constant and variable definitions "MergeLabels": reflect.ValueOf(data.MergeLabels), + "SplitYAML": reflect.ValueOf(data.SplitYAML), } } diff --git a/pkg/lib/bench_test.go b/pkg/lib/bench_test.go new file mode 100644 index 00000000..52c99628 --- /dev/null +++ b/pkg/lib/bench_test.go @@ -0,0 +1,68 @@ +package lib_test + +import ( + "context" + "io" + "testing" + + "github.com/slok/sloth/pkg/lib" +) + +func BenchmarkLibGenerateAndWrite(b *testing.B) { + const sloSpec = ` +--- +version: "prometheus/v1" +service: "myservice" +labels: + owner: "myteam" + repo: "myorg/myservice" + tier: "2" +slos: + # We allow failing (5xx and 429) 1 request every 1000 requests (99.9%). + - name: "requests-availability" + objective: 99.9 + description: "Common SLO based on availability for HTTP request responses." + labels: + category: availability + sli: + events: + error_query: sum(rate(http_request_duration_seconds_count{job="myservice",code=~"(5..|429)"}[{{.window}}])) + total_query: sum(rate(http_request_duration_seconds_count{job="myservice"}[{{.window}}])) + alerting: + name: "MyServiceHighErrorRate" + labels: + category: "availability" + annotations: + # Overwrite default Sloth SLO alert summmary on ticket and page alerts. + summary: "High error rate on 'myservice' requests responses" + page_alert: + labels: + severity: "pageteam" + routing_key: "myteam" + ticket_alert: + labels: + severity: "slack" + slack_channel: "#alerts-myteam" +` + + gen, err := lib.NewPrometheusSLOGenerator(lib.PrometheusSLOGeneratorConfig{ + ExtraLabels: map[string]string{"source": "slothlib-example"}, + }) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + ctx := context.Background() + + slo, err := gen.GenerateFromRaw(ctx, []byte(sloSpec)) + if err != nil { + b.Fatal(err) + } + + err = lib.WriteResultAsPrometheusStd(ctx, *slo, io.Discard) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/pkg/lib/gen.go b/pkg/lib/gen.go index 7b4b7eda..9b7e6456 100644 --- a/pkg/lib/gen.go +++ b/pkg/lib/gen.go @@ -34,8 +34,8 @@ var allCallerAgents = map[CallerAgent]struct{}{ CallerAgentAPI: {}, } -// Config is the configuration for the Prometheus SLO generator. -type Config struct { +// PrometheusSLOGenerator is the configuration for the Prometheus SLO generator. +type PrometheusSLOGeneratorConfig struct { // WindowsFS is the FS where custom SLO definition period windows exist (When not set default Sloth windows will be used). WindowsFS fs.FS // PluginsFS are the FSs where custom SLO and SLI plugins exist. @@ -54,7 +54,7 @@ type Config struct { Logger log.Logger } -func (c *Config) defaults() error { +func (c *PrometheusSLOGeneratorConfig) defaults() error { if c.DefaultSLOPeriod == 0 { c.DefaultSLOPeriod = 30 * 24 * time.Hour // 30 days. } @@ -88,7 +88,7 @@ type PrometheusSLOGenerator struct { agent CallerAgent } -func NewPrometheusSLOGenerator(config Config) (*PrometheusSLOGenerator, error) { +func NewPrometheusSLOGenerator(config PrometheusSLOGeneratorConfig) (*PrometheusSLOGenerator, error) { ctx := context.Background() err := config.defaults() diff --git a/pkg/lib/lib_as_cli_use_cases_test.go b/pkg/lib/lib_as_cli_use_cases_test.go index 4daf4951..4593f231 100644 --- a/pkg/lib/lib_as_cli_use_cases_test.go +++ b/pkg/lib/lib_as_cli_use_cases_test.go @@ -23,20 +23,24 @@ func TestLibAsCLIIntegration(t *testing.T) { testPluginsFS := os.DirFS("../../test/integration/prometheus/plugins") tests := map[string]struct { - config func() lib.Config + config func() lib.PrometheusSLOGeneratorConfig inFilePath string resultFormatter func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte expOutFilePath string expGenErr bool }{ "Invalid spec case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} + }, inFilePath: "../../test/integration/prometheus/testdata/in-invalid-version.yaml", expGenErr: true, }, "Prometheus case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} + }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base.yaml.tpl", resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { @@ -48,7 +52,9 @@ func TestLibAsCLIIntegration(t *testing.T) { }, "Kubernetes case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} + }, inFilePath: "../../test/integration/prometheus/testdata/in-base-k8s.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-k8s.yaml.tpl", resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { @@ -61,7 +67,9 @@ func TestLibAsCLIIntegration(t *testing.T) { }, "OpenSLO case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} + }, inFilePath: "../../test/integration/prometheus/testdata/in-openslo.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-openslo.yaml.tpl", resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { @@ -73,8 +81,8 @@ func TestLibAsCLIIntegration(t *testing.T) { }, "Default 28d window period case.": { - config: func() lib.Config { - return lib.Config{ + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{ DefaultSLOPeriod: 28 * 24 * time.Hour, CallerAgent: lib.CallerAgentCLI, } @@ -90,8 +98,8 @@ func TestLibAsCLIIntegration(t *testing.T) { }, "Custom 7d window period case.": { - config: func() lib.Config { - return lib.Config{ + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{ DefaultSLOPeriod: 7 * 24 * time.Hour, CallerAgent: lib.CallerAgentCLI, WindowsFS: testWindowsFS, @@ -108,8 +116,8 @@ func TestLibAsCLIIntegration(t *testing.T) { }, "Extra labels case.": { - config: func() lib.Config { - return lib.Config{ + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{ CallerAgent: lib.CallerAgentCLI, ExtraLabels: map[string]string{"exk1": "exv1", "exk2": "exv2"}, } @@ -125,7 +133,9 @@ func TestLibAsCLIIntegration(t *testing.T) { }, "No alerts case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} + }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-no-alerts.yaml.tpl", resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { @@ -142,7 +152,9 @@ func TestLibAsCLIIntegration(t *testing.T) { }, "No recording rules case.": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} + }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-no-recordings.yaml.tpl", resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { @@ -160,8 +172,8 @@ func TestLibAsCLIIntegration(t *testing.T) { }, "SLI plugin usage.": { - config: func() lib.Config { - return lib.Config{ + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{ CallerAgent: lib.CallerAgentCLI, PluginsFS: []fs.FS{testPluginsFS}, } @@ -177,8 +189,8 @@ func TestLibAsCLIIntegration(t *testing.T) { }, "SLO plugin usage.": { - config: func() lib.Config { - return lib.Config{ + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{ CallerAgent: lib.CallerAgentCLI, PluginsFS: []fs.FS{testPluginsFS}, } @@ -194,8 +206,8 @@ func TestLibAsCLIIntegration(t *testing.T) { }, "SLO plugin K8s usage.": { - config: func() lib.Config { - return lib.Config{ + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{ CallerAgent: lib.CallerAgentCLI, PluginsFS: []fs.FS{testPluginsFS}, } @@ -212,14 +224,18 @@ func TestLibAsCLIIntegration(t *testing.T) { }, "A multifile case (Not supported).": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} + }, inFilePath: "../../test/integration/prometheus/testdata/in-multifile.yaml", resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { return nil }, expGenErr: true, }, "A multifile Kubernetes case (Not supported).": { - config: func() lib.Config { return lib.Config{CallerAgent: lib.CallerAgentCLI} }, + config: func() lib.PrometheusSLOGeneratorConfig { + return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} + }, inFilePath: "../../test/integration/prometheus/testdata/in-multifile-k8s.yaml", resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { return nil }, expGenErr: true, diff --git a/pkg/lib/lib_test.go b/pkg/lib/lib_test.go index e2be71c3..5fe8414e 100644 --- a/pkg/lib/lib_test.go +++ b/pkg/lib/lib_test.go @@ -49,7 +49,7 @@ slos: ctx := context.Background() - gen, err := lib.NewPrometheusSLOGenerator(lib.Config{ + gen, err := lib.NewPrometheusSLOGenerator(lib.PrometheusSLOGeneratorConfig{ ExtraLabels: map[string]string{"source": "slothlib-example"}, }) if err != nil { @@ -110,7 +110,7 @@ func ExamplePrometheusSLOGenerator_GenerateFromSlothV1() { ctx := context.Background() - gen, err := lib.NewPrometheusSLOGenerator(lib.Config{ + gen, err := lib.NewPrometheusSLOGenerator(lib.PrometheusSLOGeneratorConfig{ ExtraLabels: map[string]string{"source": "slothlib-example"}, }) if err != nil { @@ -171,7 +171,7 @@ func ExamplePrometheusSLOGenerator_GenerateFromK8sV1() { ctx := context.Background() - gen, err := lib.NewPrometheusSLOGenerator(lib.Config{ + gen, err := lib.NewPrometheusSLOGenerator(lib.PrometheusSLOGeneratorConfig{ ExtraLabels: map[string]string{"source": "slothlib-example"}, }) if err != nil { diff --git a/pkg/lib/pkg_example_test.go b/pkg/lib/pkg_example_test.go index 3c0aacc2..6e5defbf 100644 --- a/pkg/lib/pkg_example_test.go +++ b/pkg/lib/pkg_example_test.go @@ -12,7 +12,7 @@ import ( func Example() { // Check with `curl -XPOST http://127.0.0.1:8080/sloth/generate -d "$(cat ./examples/getting-started.yml)"`. - gen, err := sloth.NewPrometheusSLOGenerator(sloth.Config{ + gen, err := sloth.NewPrometheusSLOGenerator(sloth.PrometheusSLOGeneratorConfig{ ExtraLabels: map[string]string{"source": "slothlib-example"}, }) if err != nil { From cd80637501bbed8d135899c1cc0ad32fbfad2759 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Wed, 22 Oct 2025 22:02:07 +0200 Subject: [PATCH 098/173] Allow adding extra prometheus group rules and customizing its name Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 2 + internal/kubernetes/modelmap/slo.go | 36 +++++- ...b_com-slok-sloth-pkg-common-conventions.go | 4 + .../storage/io/prometheus_operator_test.go | 113 +++++++++++----- internal/storage/io/std_prometheus.go | 36 +++++- internal/storage/io/std_prometheus_test.go | 112 ++++++++++++---- internal/storage/k8s/k8s_test.go | 121 +++++++++++++----- pkg/common/conventions/conventions.go | 7 + pkg/common/model/slo_prometheus.go | 9 +- 9 files changed, 347 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1575e37..c26f435a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - Sloth lib `PrometheusSLOGenerator` with `GenerateFromRaw` to generate SLOs based on any raw string spec. - Sloth lib `WriteResultAsPrometheusStd` helper method to write generated SLO results into standard Prometheus rules YAML. - Sloth lib `WriteResultAsK8sPrometheusOperator` helper method to write generated SLO results into Prometheus operator rules YAML. +- The resulting SLO Prometheus rule group name can be customized by SLO plugins. +- SLO plugins have the ability to add extra Prometheus Rule groups. ### Changed diff --git a/internal/kubernetes/modelmap/slo.go b/internal/kubernetes/modelmap/slo.go index fa2e7a11..a4350b7f 100644 --- a/internal/kubernetes/modelmap/slo.go +++ b/internal/kubernetes/modelmap/slo.go @@ -11,6 +11,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "github.com/slok/sloth/internal/storage" + "github.com/slok/sloth/pkg/common/conventions" commonerrors "github.com/slok/sloth/pkg/common/errors" promutils "github.com/slok/sloth/pkg/common/utils/prometheus" ) @@ -44,28 +45,57 @@ func MapModelToPrometheusOperator(ctx context.Context, kmeta storage.K8sMeta, sl for _, slo := range slos { if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { + name := slo.Rules.SLIErrorRecRules.Name + if name == "" { + name = conventions.PromRuleGroupNameSLOSLIPrefix + slo.SLO.ID + } rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Interval: timeDurationToPromOpDuration(slo.Rules.SLIErrorRecRules.Interval), - Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), + Name: name, Rules: promRulesToKubeRules(slo.Rules.SLIErrorRecRules.Rules), }) } if len(slo.Rules.MetadataRecRules.Rules) > 0 { + name := slo.Rules.MetadataRecRules.Name + if name == "" { + name = conventions.PromRuleGroupNameSLOMetadataPrefix + slo.SLO.ID + } rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Interval: timeDurationToPromOpDuration(slo.Rules.MetadataRecRules.Interval), - Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), + Name: name, Rules: promRulesToKubeRules(slo.Rules.MetadataRecRules.Rules), }) } if len(slo.Rules.AlertRules.Rules) > 0 { + name := slo.Rules.AlertRules.Name + if name == "" { + name = conventions.PromRuleGroupNameSLOAlertsPrefix + slo.SLO.ID + } rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Interval: timeDurationToPromOpDuration(slo.Rules.AlertRules.Interval), - Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), + Name: name, Rules: promRulesToKubeRules(slo.Rules.AlertRules.Rules), }) } + + // Extra rules. + for i, extraRuleGroup := range slo.Rules.ExtraRules { + if len(extraRuleGroup.Rules) == 0 { + continue + } + + name := extraRuleGroup.Name + if name == "" { + name = fmt.Sprintf("%s%03d-%s", conventions.PromRuleGroupNameSLOExtraRulesPrefix, i, slo.SLO.ID) + } + rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ + Interval: timeDurationToPromOpDuration(extraRuleGroup.Interval), + Name: name, + Rules: promRulesToKubeRules(extraRuleGroup.Rules), + }) + } } // If we don't have anything to store, error so we can increase the reliability diff --git a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go index 7bdcd30d..e1db5112 100644 --- a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go +++ b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-conventions.go @@ -22,6 +22,10 @@ func init() { "PromMetaSLOPeriodBurnRateRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:period_burn_rate:ratio\"", token.STRING, 0)), "PromMetaSLOPeriodErrorBudgetRemainingRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:period_error_budget_remaining:ratio\"", token.STRING, 0)), "PromMetaSLOTimePeriodDaysMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:time_period:days\"", token.STRING, 0)), + "PromRuleGroupNameSLOAlertsPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"sloth-slo-alerts-\"", token.STRING, 0)), + "PromRuleGroupNameSLOExtraRulesPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"sloth-slo-extra-rules-\"", token.STRING, 0)), + "PromRuleGroupNameSLOMetadataPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"sloth-slo-meta-recordings-\"", token.STRING, 0)), + "PromRuleGroupNameSLOSLIPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"sloth-slo-sli-recordings-\"", token.STRING, 0)), "PromSLIErrorMetricFmt": reflect.ValueOf(constant.MakeFromLiteral("\"slo:sli_error:ratio_rate%s\"", token.STRING, 0)), "PromSLOIDLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_id\"", token.STRING, 0)), "PromSLOModeLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_mode\"", token.STRING, 0)), diff --git a/internal/storage/io/prometheus_operator_test.go b/internal/storage/io/prometheus_operator_test.go index cf015429..d7f464cd 100644 --- a/internal/storage/io/prometheus_operator_test.go +++ b/internal/storage/io/prometheus_operator_test.go @@ -194,7 +194,6 @@ spec: Annotations: map[string]string{"ak1": "av1"}, }, slos: []storage.SLORulesResult{ - { SLO: model.PromSLO{ID: "testa"}, Rules: model.PromSLORules{ @@ -210,32 +209,36 @@ spec: Labels: map[string]string{"test-label": "a-2"}, }, }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-a3", - Expr: "test-expr-a3", - Labels: map[string]string{"test-label": "a-3"}, - }, - { - Record: "test:record-a4", - Expr: "test-expr-a4", - Labels: map[string]string{"test-label": "a-4"}, - }, - }}, - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlertA1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - Annotations: map[string]string{"test-annot": "a-1"}, - }, - { - Alert: "testAlertA2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - Annotations: map[string]string{"test-annot": "a-2"}, - }, - }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "custom-metadata-name-testa", // Custom name. + Rules: []rulefmt.Rule{ + { + Record: "test:record-a3", + Expr: "test-expr-a3", + Labels: map[string]string{"test-label": "a-3"}, + }, + { + Record: "test:record-a4", + Expr: "test-expr-a4", + Labels: map[string]string{"test-label": "a-4"}, + }, + }}, + AlertRules: model.PromRuleGroup{ + Interval: 15 * time.Minute, // Custom interval. + Rules: []rulefmt.Rule{ + { + Alert: "testAlertA1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + Annotations: map[string]string{"test-annot": "a-1"}, + }, + { + Alert: "testAlertA2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + Annotations: map[string]string{"test-annot": "a-2"}, + }, + }}, }, }, { @@ -263,6 +266,34 @@ spec: Annotations: map[string]string{"test-annot": "b-1"}, }, }}, + ExtraRules: []model.PromRuleGroup{ + {Interval: 42 * time.Minute, Rules: []rulefmt.Rule{ + { + Alert: "testAlertZ1", + Expr: "test-expr-z1", + Labels: map[string]string{"test-label": "z-1"}, + Annotations: map[string]string{"test-annot": "z-1"}, + }, + }}, + {}, // Should be skipped. + { + Name: "custom-test-for-extra-rules-zzzzz", + Rules: []rulefmt.Rule{ + { + Alert: "testAlertZ2", + Expr: "test-expr-z2", + Labels: map[string]string{"test-label": "z-2"}, + Annotations: map[string]string{"test-annot": "z-2"}, + }, + { + Alert: "testAlertZ3", + Expr: "test-expr-z3", + Labels: map[string]string{"test-label": "z-3"}, + Annotations: map[string]string{"test-annot": "z-3"}, + }, + }, + }, + }, }, }, }, @@ -294,7 +325,7 @@ spec: labels: test-label: a-2 record: test:record-a2 - - name: sloth-slo-meta-recordings-testa + - name: custom-metadata-name-testa rules: - expr: test-expr-a3 labels: @@ -304,7 +335,8 @@ spec: labels: test-label: a-4 record: test:record-a4 - - name: sloth-slo-alerts-testa + - interval: 15m + name: sloth-slo-alerts-testa rules: - alert: testAlertA1 annotations: @@ -338,6 +370,29 @@ spec: expr: test-expr-b1 labels: test-label: b-1 + - interval: 42m + name: sloth-slo-extra-rules-000-testb + rules: + - alert: testAlertZ1 + annotations: + test-annot: z-1 + expr: test-expr-z1 + labels: + test-label: z-1 + - name: custom-test-for-extra-rules-zzzzz + rules: + - alert: testAlertZ2 + annotations: + test-annot: z-2 + expr: test-expr-z2 + labels: + test-label: z-2 + - alert: testAlertZ3 + annotations: + test-annot: z-3 + expr: test-expr-z3 + labels: + test-label: z-3 `, }, } diff --git a/internal/storage/io/std_prometheus.go b/internal/storage/io/std_prometheus.go index c38deb46..1301af0b 100644 --- a/internal/storage/io/std_prometheus.go +++ b/internal/storage/io/std_prometheus.go @@ -10,6 +10,7 @@ import ( "gopkg.in/yaml.v2" "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/pkg/common/conventions" "github.com/slok/sloth/pkg/common/model" ) @@ -49,28 +50,57 @@ func (r StdPrometheusGroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos [ ruleGroups := stdPromRuleGroupsYAMLv2{} for _, slo := range slos { if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { + name := slo.Rules.SLIErrorRecRules.Name + if name == "" { + name = conventions.PromRuleGroupNameSLOSLIPrefix + slo.SLO.ID + } ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Interval: prommodel.Duration(slo.Rules.SLIErrorRecRules.Interval), - Name: fmt.Sprintf("sloth-slo-sli-recordings-%s", slo.SLO.ID), + Name: name, Rules: slo.Rules.SLIErrorRecRules.Rules, }) } if len(slo.Rules.MetadataRecRules.Rules) > 0 { + name := slo.Rules.MetadataRecRules.Name + if name == "" { + name = conventions.PromRuleGroupNameSLOMetadataPrefix + slo.SLO.ID + } ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Interval: prommodel.Duration(slo.Rules.MetadataRecRules.Interval), - Name: fmt.Sprintf("sloth-slo-meta-recordings-%s", slo.SLO.ID), + Name: name, Rules: slo.Rules.MetadataRecRules.Rules, }) } if len(slo.Rules.AlertRules.Rules) > 0 { + name := slo.Rules.AlertRules.Name + if name == "" { + name = conventions.PromRuleGroupNameSLOAlertsPrefix + slo.SLO.ID + } ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Interval: prommodel.Duration(slo.Rules.AlertRules.Interval), - Name: fmt.Sprintf("sloth-slo-alerts-%s", slo.SLO.ID), + Name: name, Rules: slo.Rules.AlertRules.Rules, }) } + + // Extra rules. + for i, extraRuleGroup := range slo.Rules.ExtraRules { + if len(extraRuleGroup.Rules) == 0 { + continue + } + + name := extraRuleGroup.Name + if name == "" { + name = fmt.Sprintf("%s%03d-%s", conventions.PromRuleGroupNameSLOExtraRulesPrefix, i, slo.SLO.ID) + } + ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ + Interval: prommodel.Duration(extraRuleGroup.Interval), + Name: name, + Rules: extraRuleGroup.Rules, + }) + } } // If we don't have anything to store, error so we can increase the reliability diff --git a/internal/storage/io/std_prometheus_test.go b/internal/storage/io/std_prometheus_test.go index 09439d71..77afd0d8 100644 --- a/internal/storage/io/std_prometheus_test.go +++ b/internal/storage/io/std_prometheus_test.go @@ -126,7 +126,7 @@ groups: `, }, - "Having a multiple SLO alert and recording rules should render correctly.": { + "Having a mixed example of multiple rules and options should render correctly.": { slos: []io.StdPrometheusStorageSLO{ { SLO: model.PromSLO{ID: "testa"}, @@ -143,32 +143,36 @@ groups: Labels: map[string]string{"test-label": "a-2"}, }, }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-a3", - Expr: "test-expr-a3", - Labels: map[string]string{"test-label": "a-3"}, - }, - { - Record: "test:record-a4", - Expr: "test-expr-a4", - Labels: map[string]string{"test-label": "a-4"}, - }, - }}, - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlertA1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - Annotations: map[string]string{"test-annot": "a-1"}, - }, - { - Alert: "testAlertA2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - Annotations: map[string]string{"test-annot": "a-2"}, - }, - }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "custom-metadata-name-testa", // Custom name. + Rules: []rulefmt.Rule{ + { + Record: "test:record-a3", + Expr: "test-expr-a3", + Labels: map[string]string{"test-label": "a-3"}, + }, + { + Record: "test:record-a4", + Expr: "test-expr-a4", + Labels: map[string]string{"test-label": "a-4"}, + }, + }}, + AlertRules: model.PromRuleGroup{ + Interval: 15 * time.Minute, // Custom interval. + Rules: []rulefmt.Rule{ + { + Alert: "testAlertA1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + Annotations: map[string]string{"test-annot": "a-1"}, + }, + { + Alert: "testAlertA2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + Annotations: map[string]string{"test-annot": "a-2"}, + }, + }}, }, }, { @@ -196,6 +200,34 @@ groups: Annotations: map[string]string{"test-annot": "b-1"}, }, }}, + ExtraRules: []model.PromRuleGroup{ + {Interval: 42 * time.Minute, Rules: []rulefmt.Rule{ + { + Alert: "testAlertZ1", + Expr: "test-expr-z1", + Labels: map[string]string{"test-label": "z-1"}, + Annotations: map[string]string{"test-annot": "z-1"}, + }, + }}, + {}, // Should be skipped. + { + Name: "custom-test-for-extra-rules-zzzzz", + Rules: []rulefmt.Rule{ + { + Alert: "testAlertZ2", + Expr: "test-expr-z2", + Labels: map[string]string{"test-label": "z-2"}, + Annotations: map[string]string{"test-annot": "z-2"}, + }, + { + Alert: "testAlertZ3", + Expr: "test-expr-z3", + Labels: map[string]string{"test-label": "z-3"}, + Annotations: map[string]string{"test-annot": "z-3"}, + }, + }, + }, + }, }, }, }, @@ -215,7 +247,7 @@ groups: expr: test-expr-a2 labels: test-label: a-2 -- name: sloth-slo-meta-recordings-testa +- name: custom-metadata-name-testa rules: - record: test:record-a3 expr: test-expr-a3 @@ -226,6 +258,7 @@ groups: labels: test-label: a-4 - name: sloth-slo-alerts-testa + interval: 15m rules: - alert: testAlertA1 expr: test-expr-a1 @@ -259,6 +292,29 @@ groups: test-label: b-1 annotations: test-annot: b-1 +- name: sloth-slo-extra-rules-000-testb + interval: 42m + rules: + - alert: testAlertZ1 + expr: test-expr-z1 + labels: + test-label: z-1 + annotations: + test-annot: z-1 +- name: custom-test-for-extra-rules-zzzzz + rules: + - alert: testAlertZ2 + expr: test-expr-z2 + labels: + test-label: z-2 + annotations: + test-annot: z-2 + - alert: testAlertZ3 + expr: test-expr-z3 + labels: + test-label: z-3 + annotations: + test-annot: z-3 `, }, } diff --git a/internal/storage/k8s/k8s_test.go b/internal/storage/k8s/k8s_test.go index 25b75d0f..c46f5053 100644 --- a/internal/storage/k8s/k8s_test.go +++ b/internal/storage/k8s/k8s_test.go @@ -3,6 +3,7 @@ package k8s_test import ( "context" "testing" + "time" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" monitoringclientsetfake "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" @@ -41,7 +42,7 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { expErr: true, }, - "Having multiple SLO alert and recording rules should ensure on Kubernetes correctly.": { + "Having a mixed example of multiple SLOs and options should ensure them on k8s correctly.": { k8sMeta: storage.K8sMeta{ Name: "test-name", Namespace: "test-ns", @@ -67,32 +68,36 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { Labels: map[string]string{"test-label": "a-2"}, }, }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-a3", - Expr: "test-expr-a3", - Labels: map[string]string{"test-label": "a-3"}, - }, - { - Record: "test:record-a4", - Expr: "test-expr-a4", - Labels: map[string]string{"test-label": "a-4"}, - }, - }}, - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlertA1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - Annotations: map[string]string{"test-annot": "a-1"}, - }, - { - Alert: "testAlertA2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - Annotations: map[string]string{"test-annot": "a-2"}, - }, - }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "custom-metadata-name-testa", // Custom name. + Rules: []rulefmt.Rule{ + { + Record: "test:record-a3", + Expr: "test-expr-a3", + Labels: map[string]string{"test-label": "a-3"}, + }, + { + Record: "test:record-a4", + Expr: "test-expr-a4", + Labels: map[string]string{"test-label": "a-4"}, + }, + }}, + AlertRules: model.PromRuleGroup{ + Interval: 15 * time.Minute, // Custom interval. + Rules: []rulefmt.Rule{ + { + Alert: "testAlertA1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + Annotations: map[string]string{"test-annot": "a-1"}, + }, + { + Alert: "testAlertA2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + Annotations: map[string]string{"test-annot": "a-2"}, + }, + }}, }, }, { @@ -120,6 +125,34 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { Annotations: map[string]string{"test-annot": "b-1"}, }, }}, + ExtraRules: []model.PromRuleGroup{ + {Interval: 42 * time.Minute, Rules: []rulefmt.Rule{ + { + Alert: "testAlertZ1", + Expr: "test-expr-z1", + Labels: map[string]string{"test-label": "z-1"}, + Annotations: map[string]string{"test-annot": "z-1"}, + }, + }}, + {}, // Should be skipped. + { + Name: "custom-test-for-extra-rules-zzzzz", + Rules: []rulefmt.Rule{ + { + Alert: "testAlertZ2", + Expr: "test-expr-z2", + Labels: map[string]string{"test-label": "z-2"}, + Annotations: map[string]string{"test-annot": "z-2"}, + }, + { + Alert: "testAlertZ3", + Expr: "test-expr-z3", + Labels: map[string]string{"test-label": "z-3"}, + Annotations: map[string]string{"test-annot": "z-3"}, + }, + }, + }, + }, }, }, }, @@ -165,7 +198,7 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { }, }, { - Name: "sloth-slo-meta-recordings-testa", + Name: "custom-metadata-name-testa", Rules: []monitoringv1.Rule{ { Record: "test:record-a3", @@ -180,7 +213,8 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { }, }, { - Name: "sloth-slo-alerts-testa", + Name: "sloth-slo-alerts-testa", + Interval: &([]monitoringv1.Duration{monitoringv1.Duration("15m")}[0]), Rules: []monitoringv1.Rule{ { Alert: "testAlertA1", @@ -227,6 +261,35 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { }, }, }, + { + Name: "sloth-slo-extra-rules-000-testb", + Interval: &([]monitoringv1.Duration{monitoringv1.Duration("42m")}[0]), + Rules: []monitoringv1.Rule{ + { + Alert: "testAlertZ1", + Expr: intstr.FromString("test-expr-z1"), + Labels: map[string]string{"test-label": "z-1"}, + Annotations: map[string]string{"test-annot": "z-1"}, + }, + }, + }, + { + Name: "custom-test-for-extra-rules-zzzzz", + Rules: []monitoringv1.Rule{ + { + Alert: "testAlertZ2", + Expr: intstr.FromString("test-expr-z2"), + Labels: map[string]string{"test-label": "z-2"}, + Annotations: map[string]string{"test-annot": "z-2"}, + }, + { + Alert: "testAlertZ3", + Expr: intstr.FromString("test-expr-z3"), + Labels: map[string]string{"test-label": "z-3"}, + Annotations: map[string]string{"test-annot": "z-3"}, + }, + }, + }, }, }, }, diff --git a/pkg/common/conventions/conventions.go b/pkg/common/conventions/conventions.go index 68772cbd..7cce7d4d 100644 --- a/pkg/common/conventions/conventions.go +++ b/pkg/common/conventions/conventions.go @@ -15,3 +15,10 @@ var ( // TplSLIQueryWindowVarName is the name of the window template variable used in the SLI queries. TplSLIQueryWindowVarName = "window" ) + +const ( + PromRuleGroupNameSLOSLIPrefix = "sloth-slo-sli-recordings-" + PromRuleGroupNameSLOMetadataPrefix = "sloth-slo-meta-recordings-" + PromRuleGroupNameSLOAlertsPrefix = "sloth-slo-alerts-" + PromRuleGroupNameSLOExtraRulesPrefix = "sloth-slo-extra-rules-" +) diff --git a/pkg/common/model/slo_prometheus.go b/pkg/common/model/slo_prometheus.go index fd17590f..a1bf357b 100644 --- a/pkg/common/model/slo_prometheus.go +++ b/pkg/common/model/slo_prometheus.go @@ -74,13 +74,20 @@ type PromSLOGroupSource struct { // PromSLORules are the prometheus rules required by an SLO. type PromSLORules struct { + // SLIErrorRecRules are the rules for the SLI error recording rules. SLIErrorRecRules PromRuleGroup + // MetadataRecRules are the rules for the metadata recording rules. MetadataRecRules PromRuleGroup - AlertRules PromRuleGroup + // AlertRules are the rules for the SLO alerting rules. + AlertRules PromRuleGroup + // ExtraRules are the extra rules for the SLO, normally used for custom use cases required by the SLO plugins. + ExtraRules []PromRuleGroup } // PromRuleGroup are regular prometheus group of rules. type PromRuleGroup struct { + // Name is the name of the rule group. If empty, a default name will be generated. + Name string Interval time.Duration Rules []rulefmt.Rule } From 5bf5017249a553d835c147d5a4ccabdaba978dd3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 07:01:57 +0000 Subject: [PATCH 099/173] build(deps): bump github.com/prometheus/prometheus Bumps [github.com/prometheus/prometheus](https://github.com/prometheus/prometheus) from 0.306.0 to 0.307.2. - [Release notes](https://github.com/prometheus/prometheus/releases) - [Changelog](https://github.com/prometheus/prometheus/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/prometheus/compare/v0.306.0...v0.307.2) --- updated-dependencies: - dependency-name: github.com/prometheus/prometheus dependency-version: 0.307.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 3 +- go.sum | 114 +++++++++++++++++++++++++++++---------------------------- 2 files changed, 59 insertions(+), 58 deletions(-) diff --git a/go.mod b/go.mod index 8fd274bd..95aeb7cf 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.1 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/common v0.67.1 - github.com/prometheus/prometheus v0.306.0 + github.com/prometheus/prometheus v0.307.2 github.com/sirupsen/logrus v1.9.3 github.com/slok/reload v0.2.0 github.com/spotahome/kooper/v2 v2.9.0 @@ -77,7 +77,6 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 // indirect diff --git a/go.sum b/go.sum index f8aae4cc..957e7428 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,17 @@ -cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4= -cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA= +cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +cloud.google.com/go/compute/metadata v0.8.4 h1:oXMa1VMQBVCyewMIOm3WQsnVd9FbKBtm8reqWRaXnHQ= +cloud.google.com/go/compute/metadata v0.8.4/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0 h1:wL5IEG5zb7BVv1Kv0Xm92orq+5hB5Nipn3B5tn4Rqfk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.12.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 h1:XkkQbfMyuH2jTSjQjSoihryI8GINRcs4xp8lNawg0FI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/OpenSLO/oslo v0.12.0 h1:0zdxMgFE59TUKpe/L4+5ujgmJZW/kAuCOJbyqrTX4lc= github.com/OpenSLO/oslo v0.12.0/go.mod h1:6jyOTkqBCdkgqLXJ6WtJj7+o0rSexl6YZbWwnxpGwtU= github.com/VictoriaMetrics/metrics v1.40.2 h1:OVSjKcQEx6JAwGeu8/KQm9Su5qJ72TMEW4xYn5vw3Ac= @@ -22,32 +22,32 @@ github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjH github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= -github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= -github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= -github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= -github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= -github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= -github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= +github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= +github.com/aws/aws-sdk-go-v2/config v1.31.12 h1:pYM1Qgy0dKZLHX2cXslNacbcEFMkDMl+Bcj5ROuS6p8= +github.com/aws/aws-sdk-go-v2/config v1.31.12/go.mod h1:/MM0dyD7KSDPR+39p9ZNVKaHDLb9qnfDurvVS2KAhN8= +github.com/aws/aws-sdk-go-v2/credentials v1.18.16 h1:4JHirI4zp958zC026Sm+V4pSDwW4pwLefKrc0bF2lwI= +github.com/aws/aws-sdk-go-v2/credentials v1.18.16/go.mod h1:qQMtGx9OSw7ty1yLclzLxXCRbrkjWAM7JnObZjmCB7I= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9 h1:Mv4Bc0mWmv6oDuSWTKnk+wgeqPL5DRFu5bQL9BGPQ8Y= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.9/go.mod h1:IKlKfRppK2a1y0gy1yH6zD+yX5uplJ6UuPlgd48dJiQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= -github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9 h1:5r34CgVOD4WZudeEKZ9/iKpiT6cM1JyEROpXjOcdWv8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.9/go.mod h1:dB12CEbNWPbzO2uC6QSWHteqOg4JfBVJOojbAoAUb5I= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.6 h1:A1oRkiSQOWstGh61y4Wc/yQ04sqrQZr1Si/oAXj20/s= +github.com/aws/aws-sdk-go-v2/service/sso v1.29.6/go.mod h1:5PfYspyCU5Vw1wNPsxi15LZovOnULudOQuVxphSflQA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1 h1:5fm5RTONng73/QA73LhCNR7UT9RpFH3hR6HWL6bIgVY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.1/go.mod h1:xBEjWD13h+6nq+z4AkqSfSvqRKFgDIQeaMguAJndOWo= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 h1:p3jIvqYwUZgu/XYeI48bJxOhvm47hZb5HUQ0tn6Q9kA= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.6/go.mod h1:WtKK+ppze5yKPkZ0XwqIVWD4beCwv056ZbPQNoeHqM8= +github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= +github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -111,8 +111,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -120,16 +120,16 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18= -github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8 h1:ZI8gCoCjGzPsum4L21jHdQs8shFBIQih1TM9Rd/c+EQ= +github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0= -github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= @@ -180,12 +180,14 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= -github.com/prometheus/prometheus v0.306.0 h1:Q0Pvz/ZKS6vVWCa1VSgNyNJlEe8hxdRlKklFg7SRhNw= -github.com/prometheus/prometheus v0.306.0/go.mod h1:7hMSGyZHt0dcmZ5r4kFPJ/vxPQU99N5/BGwSPDxeZrQ= -github.com/prometheus/sigv4 v0.2.0 h1:qDFKnHYFswJxdzGeRP63c4HlH3Vbn1Yf/Ao2zabtVXk= -github.com/prometheus/sigv4 v0.2.0/go.mod h1:D04rqmAaPPEUkjRQxGqjoxdyJuyCh6E0M18fZr0zBiE= +github.com/prometheus/prometheus v0.307.2 h1:oA6J+sgS1iTEpsRjyKUYAe+3BzwpsYBqzeRchPOUZ/M= +github.com/prometheus/prometheus v0.307.2/go.mod h1:UeEsqN3iSmAASRE3qkAm3b/3ofdiTAqGdsz+Sj3F0KA= +github.com/prometheus/sigv4 v0.2.1 h1:hl8D3+QEzU9rRmbKIRwMKRwaFGyLkbPdH5ZerglRHY0= +github.com/prometheus/sigv4 v0.2.1/go.mod h1:ySk6TahIlsR2sxADuHy4IBFhwEjRGGsfbbLGhFYFj6Q= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -224,8 +226,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= @@ -245,8 +247,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90= +golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= 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/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -286,12 +288,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo= -google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= +google.golang.org/api v0.250.0 h1:qvkwrf/raASj82UegU2RSDGWi/89WkLckn4LuO4lVXM= +google.golang.org/api v0.250.0/go.mod h1:Y9Uup8bDLJJtMzJyQnu+rLRJLA0wn+wTtc6vTlOvfXo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= +google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From aa91865108e57d91c3031f91979eae4dd542770b Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Fri, 24 Oct 2025 18:52:30 +0200 Subject: [PATCH 100/173] Improve how default generated rule group names are set Signed-off-by: Xabier Larrakoetxea --- internal/app/generate/generate.go | 23 + internal/app/generate/generate_test.go | 753 +++++++++--------- internal/kubernetes/modelmap/slo.go | 27 +- .../storage/io/prometheus_operator_test.go | 135 ++-- internal/storage/io/std_prometheus.go | 30 +- internal/storage/io/std_prometheus_test.go | 132 +-- internal/storage/k8s/k8s_test.go | 100 ++- 7 files changed, 625 insertions(+), 575 deletions(-) diff --git a/internal/app/generate/generate.go b/internal/app/generate/generate.go index 8ae4487c..8906ac3f 100644 --- a/internal/app/generate/generate.go +++ b/internal/app/generate/generate.go @@ -14,6 +14,7 @@ import ( plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" plugincorevalidatev1 "github.com/slok/sloth/internal/plugin/slo/core/validate_v1" pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" + "github.com/slok/sloth/pkg/common/conventions" commonerrors "github.com/slok/sloth/pkg/common/errors" "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" @@ -172,6 +173,9 @@ func (s Service) Generate(ctx context.Context, r Request) (*Response, error) { return nil, fmt.Errorf("could not generate %q slo: %w", slo.ID, err) } + // Set safe defaults on rules result. + setDefaultsPromSLORulesResult(slo, result) + results = append(results, SLOResult{SLO: slo, SLORules: *result}) } @@ -272,3 +276,22 @@ func (s Service) validateSLOGroup(sloGroup model.PromSLOGroup) error { return nil } + +// helper function to set the required safe defaults generated SLO prom rules. +func setDefaultsPromSLORulesResult(slo model.PromSLO, rules *model.PromSLORules) { + // Set rule result naming defaults. + if rules.SLIErrorRecRules.Name == "" { + rules.SLIErrorRecRules.Name = conventions.PromRuleGroupNameSLOSLIPrefix + slo.ID + } + if rules.MetadataRecRules.Name == "" { + rules.MetadataRecRules.Name = conventions.PromRuleGroupNameSLOMetadataPrefix + slo.ID + } + if rules.AlertRules.Name == "" { + rules.AlertRules.Name = conventions.PromRuleGroupNameSLOAlertsPrefix + slo.ID + } + for i, extraRuleGroup := range rules.ExtraRules { + if extraRuleGroup.Name == "" { + extraRuleGroup.Name = fmt.Sprintf("%s%03d-%s", conventions.PromRuleGroupNameSLOExtraRulesPrefix, i, slo.ID) + } + } +} diff --git a/internal/app/generate/generate_test.go b/internal/app/generate/generate_test.go index 08044da1..a387ac1d 100644 --- a/internal/app/generate/generate_test.go +++ b/internal/app/generate/generate_test.go @@ -175,210 +175,215 @@ func TestIntegrationAppServiceGenerate(t *testing.T) { }, }, SLORules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "slo:sli_error:ratio_rate5m", - Expr: "(rate(my_metric{error=\"true\"}[5m]))\n/\n(rate(my_metric[5m]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "5m", + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-test-id", + Rules: []rulefmt.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Expr: "(rate(my_metric{error=\"true\"}[5m]))\n/\n(rate(my_metric[5m]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "5m", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate30m", - Expr: "(rate(my_metric{error=\"true\"}[30m]))\n/\n(rate(my_metric[30m]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "30m", + { + Record: "slo:sli_error:ratio_rate30m", + Expr: "(rate(my_metric{error=\"true\"}[30m]))\n/\n(rate(my_metric[30m]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "30m", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate1h", - Expr: "(rate(my_metric{error=\"true\"}[1h]))\n/\n(rate(my_metric[1h]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "1h", + { + Record: "slo:sli_error:ratio_rate1h", + Expr: "(rate(my_metric{error=\"true\"}[1h]))\n/\n(rate(my_metric[1h]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "1h", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate2h", - Expr: "(rate(my_metric{error=\"true\"}[2h]))\n/\n(rate(my_metric[2h]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "2h", + { + Record: "slo:sli_error:ratio_rate2h", + Expr: "(rate(my_metric{error=\"true\"}[2h]))\n/\n(rate(my_metric[2h]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "2h", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate6h", - Expr: "(rate(my_metric{error=\"true\"}[6h]))\n/\n(rate(my_metric[6h]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "6h", + { + Record: "slo:sli_error:ratio_rate6h", + Expr: "(rate(my_metric{error=\"true\"}[6h]))\n/\n(rate(my_metric[6h]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "6h", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate1d", - Expr: "(rate(my_metric{error=\"true\"}[1d]))\n/\n(rate(my_metric[1d]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "1d", + { + Record: "slo:sli_error:ratio_rate1d", + Expr: "(rate(my_metric{error=\"true\"}[1d]))\n/\n(rate(my_metric[1d]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "1d", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate3d", - Expr: "(rate(my_metric{error=\"true\"}[3d]))\n/\n(rate(my_metric[3d]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "3d", + { + Record: "slo:sli_error:ratio_rate3d", + Expr: "(rate(my_metric{error=\"true\"}[3d]))\n/\n(rate(my_metric[3d]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "3d", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate30d", - Expr: "sum_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"test-id\", sloth_service=\"test-svc\", sloth_slo=\"test-name\"}[30d])\n/ ignoring (sloth_window)\ncount_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"test-id\", sloth_service=\"test-svc\", sloth_slo=\"test-name\"}[30d])\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "30d", + { + Record: "slo:sli_error:ratio_rate30d", + Expr: "sum_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"test-id\", sloth_service=\"test-svc\", sloth_slo=\"test-name\"}[30d])\n/ ignoring (sloth_window)\ncount_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"test-id\", sloth_service=\"test-svc\", sloth_slo=\"test-name\"}[30d])\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "30d", + }, }, - }, - }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - // Metadata labels. - { - Record: "slo:objective:ratio", - Expr: "vector(0.9990000000000001)", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-test-id", + Rules: []rulefmt.Rule{ + // Metadata labels. + { + Record: "slo:objective:ratio", + Expr: "vector(0.9990000000000001)", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "slo:error_budget:ratio", - Expr: "vector(1-0.9990000000000001)", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + { + Record: "slo:error_budget:ratio", + Expr: "vector(1-0.9990000000000001)", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "slo:time_period:days", - Expr: "vector(30)", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + { + Record: "slo:time_period:days", + Expr: "vector(30)", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "slo:current_burn_rate:ratio", - Expr: `slo:sli_error:ratio_rate5m{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} + { + Record: "slo:current_burn_rate:ratio", + Expr: `slo:sli_error:ratio_rate5m{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} / on(sloth_id, sloth_slo, sloth_service) group_left slo:error_budget:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} `, - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "slo:period_burn_rate:ratio", - Expr: `slo:sli_error:ratio_rate30d{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} + { + Record: "slo:period_burn_rate:ratio", + Expr: `slo:sli_error:ratio_rate30d{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} / on(sloth_id, sloth_slo, sloth_service) group_left slo:error_budget:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} `, - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "slo:period_error_budget_remaining:ratio", - Expr: `1 - slo:period_burn_rate:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"}`, - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + { + Record: "slo:period_error_budget_remaining:ratio", + Expr: `1 - slo:period_burn_rate:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"}`, + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "sloth_slo_info", - Expr: `vector(1)`, - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_mode": "test", - "sloth_version": "test-ver", - "sloth_spec": "test-spec", - "sloth_objective": "99.9", + { + Record: "sloth_slo_info", + Expr: `vector(1)`, + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_mode": "test", + "sloth_version": "test-ver", + "sloth_spec": "test-spec", + "sloth_objective": "99.9", + }, }, - }, - }}, + }}, AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-test-id", Interval: 99 * time.Minute, // From the SLO plugins. Rules: []rulefmt.Rule{ { @@ -583,210 +588,215 @@ or }, }, SLORules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "slo:sli_error:ratio_rate5m", - Expr: "(rate(my_metric{error=\"true\"}[5m]))\n/\n(rate(my_metric[5m]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "5m", + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-test-id", + Rules: []rulefmt.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Expr: "(rate(my_metric{error=\"true\"}[5m]))\n/\n(rate(my_metric[5m]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "5m", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate30m", - Expr: "(rate(my_metric{error=\"true\"}[30m]))\n/\n(rate(my_metric[30m]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "30m", + { + Record: "slo:sli_error:ratio_rate30m", + Expr: "(rate(my_metric{error=\"true\"}[30m]))\n/\n(rate(my_metric[30m]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "30m", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate1h", - Expr: "(rate(my_metric{error=\"true\"}[1h]))\n/\n(rate(my_metric[1h]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "1h", + { + Record: "slo:sli_error:ratio_rate1h", + Expr: "(rate(my_metric{error=\"true\"}[1h]))\n/\n(rate(my_metric[1h]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "1h", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate2h", - Expr: "(rate(my_metric{error=\"true\"}[2h]))\n/\n(rate(my_metric[2h]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "2h", + { + Record: "slo:sli_error:ratio_rate2h", + Expr: "(rate(my_metric{error=\"true\"}[2h]))\n/\n(rate(my_metric[2h]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "2h", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate6h", - Expr: "(rate(my_metric{error=\"true\"}[6h]))\n/\n(rate(my_metric[6h]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "6h", + { + Record: "slo:sli_error:ratio_rate6h", + Expr: "(rate(my_metric{error=\"true\"}[6h]))\n/\n(rate(my_metric[6h]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "6h", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate1d", - Expr: "(rate(my_metric{error=\"true\"}[1d]))\n/\n(rate(my_metric[1d]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "1d", + { + Record: "slo:sli_error:ratio_rate1d", + Expr: "(rate(my_metric{error=\"true\"}[1d]))\n/\n(rate(my_metric[1d]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "1d", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate3d", - Expr: "(rate(my_metric{error=\"true\"}[3d]))\n/\n(rate(my_metric[3d]))\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "3d", + { + Record: "slo:sli_error:ratio_rate3d", + Expr: "(rate(my_metric{error=\"true\"}[3d]))\n/\n(rate(my_metric[3d]))\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "3d", + }, }, - }, - { - Record: "slo:sli_error:ratio_rate30d", - Expr: "sum_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"test-id\", sloth_service=\"test-svc\", sloth_slo=\"test-name\"}[30d])\n/ ignoring (sloth_window)\ncount_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"test-id\", sloth_service=\"test-svc\", sloth_slo=\"test-name\"}[30d])\n", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_window": "30d", + { + Record: "slo:sli_error:ratio_rate30d", + Expr: "sum_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"test-id\", sloth_service=\"test-svc\", sloth_slo=\"test-name\"}[30d])\n/ ignoring (sloth_window)\ncount_over_time(slo:sli_error:ratio_rate5m{sloth_id=\"test-id\", sloth_service=\"test-svc\", sloth_slo=\"test-name\"}[30d])\n", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_window": "30d", + }, }, - }, - }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - // Metadata labels. - { - Record: "slo:objective:ratio", - Expr: "vector(0.9990000000000001)", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-test-id", + Rules: []rulefmt.Rule{ + // Metadata labels. + { + Record: "slo:objective:ratio", + Expr: "vector(0.9990000000000001)", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "slo:error_budget:ratio", - Expr: "vector(1-0.9990000000000001)", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + { + Record: "slo:error_budget:ratio", + Expr: "vector(1-0.9990000000000001)", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "slo:time_period:days", - Expr: "vector(30)", - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + { + Record: "slo:time_period:days", + Expr: "vector(30)", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "slo:current_burn_rate:ratio", - Expr: `slo:sli_error:ratio_rate5m{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} + { + Record: "slo:current_burn_rate:ratio", + Expr: `slo:sli_error:ratio_rate5m{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} / on(sloth_id, sloth_slo, sloth_service) group_left slo:error_budget:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} `, - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "slo:period_burn_rate:ratio", - Expr: `slo:sli_error:ratio_rate30d{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} + { + Record: "slo:period_burn_rate:ratio", + Expr: `slo:sli_error:ratio_rate30d{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} / on(sloth_id, sloth_slo, sloth_service) group_left slo:error_budget:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"} `, - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "slo:period_error_budget_remaining:ratio", - Expr: `1 - slo:period_burn_rate:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"}`, - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", + { + Record: "slo:period_error_budget_remaining:ratio", + Expr: `1 - slo:period_burn_rate:ratio{sloth_id="test-id", sloth_service="test-svc", sloth_slo="test-name"}`, + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + }, }, - }, - { - Record: "sloth_slo_info", - Expr: `vector(1)`, - Labels: map[string]string{ - "test_label": "label_1", - "extra_k1": "extra_v1", - "extra_k2": "extra_v2", - "sloth_service": "test-svc", - "sloth_slo": "test-name", - "sloth_id": "test-id", - "sloth_mode": "test", - "sloth_version": "test-ver", - "sloth_spec": "test-spec", - "sloth_objective": "99.9", + { + Record: "sloth_slo_info", + Expr: `vector(1)`, + Labels: map[string]string{ + "test_label": "label_1", + "extra_k1": "extra_v1", + "extra_k2": "extra_v2", + "sloth_service": "test-svc", + "sloth_slo": "test-name", + "sloth_id": "test-id", + "sloth_mode": "test", + "sloth_version": "test-ver", + "sloth_spec": "test-spec", + "sloth_objective": "99.9", + }, }, - }, - }}, + }}, AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-test-id", Rules: []rulefmt.Rule{ { Alert: "p_alert_test_name", @@ -903,9 +913,18 @@ or }, }, SLORules: model.PromSLORules{ - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - {Expr: "test1"}, - }}, + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-test-id", + }, + MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-test-id", + }, + AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-test-id", + Rules: []rulefmt.Rule{ + {Expr: "test1"}, + }, + }, }, }, }, diff --git a/internal/kubernetes/modelmap/slo.go b/internal/kubernetes/modelmap/slo.go index a4350b7f..83c27440 100644 --- a/internal/kubernetes/modelmap/slo.go +++ b/internal/kubernetes/modelmap/slo.go @@ -11,7 +11,6 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "github.com/slok/sloth/internal/storage" - "github.com/slok/sloth/pkg/common/conventions" commonerrors "github.com/slok/sloth/pkg/common/errors" promutils "github.com/slok/sloth/pkg/common/utils/prometheus" ) @@ -45,54 +44,38 @@ func MapModelToPrometheusOperator(ctx context.Context, kmeta storage.K8sMeta, sl for _, slo := range slos { if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { - name := slo.Rules.SLIErrorRecRules.Name - if name == "" { - name = conventions.PromRuleGroupNameSLOSLIPrefix + slo.SLO.ID - } rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Interval: timeDurationToPromOpDuration(slo.Rules.SLIErrorRecRules.Interval), - Name: name, + Name: slo.Rules.SLIErrorRecRules.Name, Rules: promRulesToKubeRules(slo.Rules.SLIErrorRecRules.Rules), }) } if len(slo.Rules.MetadataRecRules.Rules) > 0 { - name := slo.Rules.MetadataRecRules.Name - if name == "" { - name = conventions.PromRuleGroupNameSLOMetadataPrefix + slo.SLO.ID - } rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Interval: timeDurationToPromOpDuration(slo.Rules.MetadataRecRules.Interval), - Name: name, + Name: slo.Rules.MetadataRecRules.Name, Rules: promRulesToKubeRules(slo.Rules.MetadataRecRules.Rules), }) } if len(slo.Rules.AlertRules.Rules) > 0 { - name := slo.Rules.AlertRules.Name - if name == "" { - name = conventions.PromRuleGroupNameSLOAlertsPrefix + slo.SLO.ID - } rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Interval: timeDurationToPromOpDuration(slo.Rules.AlertRules.Interval), - Name: name, + Name: slo.Rules.AlertRules.Name, Rules: promRulesToKubeRules(slo.Rules.AlertRules.Rules), }) } // Extra rules. - for i, extraRuleGroup := range slo.Rules.ExtraRules { + for _, extraRuleGroup := range slo.Rules.ExtraRules { if len(extraRuleGroup.Rules) == 0 { continue } - name := extraRuleGroup.Name - if name == "" { - name = fmt.Sprintf("%s%03d-%s", conventions.PromRuleGroupNameSLOExtraRulesPrefix, i, slo.SLO.ID) - } rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ Interval: timeDurationToPromOpDuration(extraRuleGroup.Interval), - Name: name, + Name: extraRuleGroup.Name, Rules: promRulesToKubeRules(extraRuleGroup.Rules), }) } diff --git a/internal/storage/io/prometheus_operator_test.go b/internal/storage/io/prometheus_operator_test.go index d7f464cd..45372014 100644 --- a/internal/storage/io/prometheus_operator_test.go +++ b/internal/storage/io/prometheus_operator_test.go @@ -47,13 +47,15 @@ func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - }, - }}, + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-test1", + Rules: []rulefmt.Rule{ + { + Record: "test:record", + Expr: "test-expr", + Labels: map[string]string{"test-label": "one"}, + }, + }}, }, }, }, @@ -96,6 +98,7 @@ spec: SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-test1", Interval: 42 * time.Minute, Rules: []rulefmt.Rule{ { @@ -146,14 +149,16 @@ spec: { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlert", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - Annotations: map[string]string{"test-annot": "one"}, - }, - }}, + AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-test1", + Rules: []rulefmt.Rule{ + { + Alert: "testAlert", + Expr: "test-expr", + Labels: map[string]string{"test-label": "one"}, + Annotations: map[string]string{"test-annot": "one"}, + }, + }}, }, }, }, @@ -197,20 +202,22 @@ spec: { SLO: model.PromSLO{ID: "testa"}, Rules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-a1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - }, - { - Record: "test:record-a2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - }, - }}, + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-testa", + Rules: []rulefmt.Rule{ + { + Record: "test:record-a1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + }, + { + Record: "test:record-a2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + }, + }}, MetadataRecRules: model.PromRuleGroup{ - Name: "custom-metadata-name-testa", // Custom name. + Name: "sloth-slo-meta-recordings-testa", Rules: []rulefmt.Rule{ { Record: "test:record-a3", @@ -224,6 +231,7 @@ spec: }, }}, AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-testa", Interval: 15 * time.Minute, // Custom interval. Rules: []rulefmt.Rule{ { @@ -244,40 +252,49 @@ spec: { SLO: model.PromSLO{ID: "testb"}, Rules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-b1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - }, - }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-b2", - Expr: "test-expr-b2", - Labels: map[string]string{"test-label": "b-2"}, - }, - }}, - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlertB1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - Annotations: map[string]string{"test-annot": "b-1"}, - }, - }}, - ExtraRules: []model.PromRuleGroup{ - {Interval: 42 * time.Minute, Rules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-testb", + Rules: []rulefmt.Rule{ + { + Record: "test:record-b1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + }, + }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-testb", + Rules: []rulefmt.Rule{ + { + Record: "test:record-b2", + Expr: "test-expr-b2", + Labels: map[string]string{"test-label": "b-2"}, + }, + }}, + AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-testb", + Rules: []rulefmt.Rule{ { - Alert: "testAlertZ1", - Expr: "test-expr-z1", - Labels: map[string]string{"test-label": "z-1"}, - Annotations: map[string]string{"test-annot": "z-1"}, + Alert: "testAlertB1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + Annotations: map[string]string{"test-annot": "b-1"}, }, }}, + ExtraRules: []model.PromRuleGroup{ + { + Name: "sloth-slo-extra-rules-000-testb", + Interval: 42 * time.Minute, + Rules: []rulefmt.Rule{ + { + Alert: "testAlertZ1", + Expr: "test-expr-z1", + Labels: map[string]string{"test-label": "z-1"}, + Annotations: map[string]string{"test-annot": "z-1"}, + }, + }}, {}, // Should be skipped. { - Name: "custom-test-for-extra-rules-zzzzz", + Name: "sloth-slo-extra-rules-001-testb", Rules: []rulefmt.Rule{ { Alert: "testAlertZ2", @@ -325,7 +342,7 @@ spec: labels: test-label: a-2 record: test:record-a2 - - name: custom-metadata-name-testa + - name: sloth-slo-meta-recordings-testa rules: - expr: test-expr-a3 labels: @@ -379,7 +396,7 @@ spec: expr: test-expr-z1 labels: test-label: z-1 - - name: custom-test-for-extra-rules-zzzzz + - name: sloth-slo-extra-rules-001-testb rules: - alert: testAlertZ2 annotations: diff --git a/internal/storage/io/std_prometheus.go b/internal/storage/io/std_prometheus.go index 1301af0b..f01c33a0 100644 --- a/internal/storage/io/std_prometheus.go +++ b/internal/storage/io/std_prometheus.go @@ -10,7 +10,6 @@ import ( "gopkg.in/yaml.v2" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/pkg/common/conventions" "github.com/slok/sloth/pkg/common/model" ) @@ -50,54 +49,38 @@ func (r StdPrometheusGroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos [ ruleGroups := stdPromRuleGroupsYAMLv2{} for _, slo := range slos { if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { - name := slo.Rules.SLIErrorRecRules.Name - if name == "" { - name = conventions.PromRuleGroupNameSLOSLIPrefix + slo.SLO.ID - } ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Interval: prommodel.Duration(slo.Rules.SLIErrorRecRules.Interval), - Name: name, + Name: slo.Rules.SLIErrorRecRules.Name, Rules: slo.Rules.SLIErrorRecRules.Rules, }) } if len(slo.Rules.MetadataRecRules.Rules) > 0 { - name := slo.Rules.MetadataRecRules.Name - if name == "" { - name = conventions.PromRuleGroupNameSLOMetadataPrefix + slo.SLO.ID - } ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Interval: prommodel.Duration(slo.Rules.MetadataRecRules.Interval), - Name: name, + Name: slo.Rules.MetadataRecRules.Name, Rules: slo.Rules.MetadataRecRules.Rules, }) } if len(slo.Rules.AlertRules.Rules) > 0 { - name := slo.Rules.AlertRules.Name - if name == "" { - name = conventions.PromRuleGroupNameSLOAlertsPrefix + slo.SLO.ID - } ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Interval: prommodel.Duration(slo.Rules.AlertRules.Interval), - Name: name, + Name: slo.Rules.AlertRules.Name, Rules: slo.Rules.AlertRules.Rules, }) } // Extra rules. - for i, extraRuleGroup := range slo.Rules.ExtraRules { + for _, extraRuleGroup := range slo.Rules.ExtraRules { if len(extraRuleGroup.Rules) == 0 { continue } - name := extraRuleGroup.Name - if name == "" { - name = fmt.Sprintf("%s%03d-%s", conventions.PromRuleGroupNameSLOExtraRulesPrefix, i, slo.SLO.ID) - } ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ Interval: prommodel.Duration(extraRuleGroup.Interval), - Name: name, + Name: extraRuleGroup.Name, Rules: extraRuleGroup.Rules, }) } @@ -121,9 +104,6 @@ func (r StdPrometheusGroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos [ return fmt.Errorf("could not write top disclaimer: %w", err) } - logger := r.logger.WithCtxValues(ctx) - logger.WithValues(log.Kv{"groups": len(ruleGroups.Groups)}).Infof("Prometheus rules written") - return nil } diff --git a/internal/storage/io/std_prometheus_test.go b/internal/storage/io/std_prometheus_test.go index 77afd0d8..b1325c5c 100644 --- a/internal/storage/io/std_prometheus_test.go +++ b/internal/storage/io/std_prometheus_test.go @@ -37,13 +37,15 @@ func TestGroupedRulesYAMLRepoStore(t *testing.T) { { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - }, - }}, + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-test1", + Rules: []rulefmt.Rule{ + { + Record: "test:record", + Expr: "test-expr", + Labels: map[string]string{"test-label": "one"}, + }, + }}, }, }, }, @@ -66,13 +68,15 @@ groups: { SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - }, - }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-test1", + Rules: []rulefmt.Rule{ + { + Record: "test:record", + Expr: "test-expr", + Labels: map[string]string{"test-label": "one"}, + }, + }}, }, }, }, @@ -96,6 +100,7 @@ groups: SLO: model.PromSLO{ID: "test1"}, Rules: model.PromSLORules{ AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-test1", Interval: 42 * time.Minute, Rules: []rulefmt.Rule{ { @@ -131,20 +136,22 @@ groups: { SLO: model.PromSLO{ID: "testa"}, Rules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-a1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - }, - { - Record: "test:record-a2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - }, - }}, + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-testa", + Rules: []rulefmt.Rule{ + { + Record: "test:record-a1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + }, + { + Record: "test:record-a2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + }, + }}, MetadataRecRules: model.PromRuleGroup{ - Name: "custom-metadata-name-testa", // Custom name. + Name: "sloth-slo-meta-recordings-testa", Rules: []rulefmt.Rule{ { Record: "test:record-a3", @@ -158,6 +165,7 @@ groups: }, }}, AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-testa", Interval: 15 * time.Minute, // Custom interval. Rules: []rulefmt.Rule{ { @@ -178,40 +186,48 @@ groups: { SLO: model.PromSLO{ID: "testb"}, Rules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-b1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - }, - }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-b2", - Expr: "test-expr-b2", - Labels: map[string]string{"test-label": "b-2"}, - }, - }}, - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlertB1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - Annotations: map[string]string{"test-annot": "b-1"}, - }, - }}, - ExtraRules: []model.PromRuleGroup{ - {Interval: 42 * time.Minute, Rules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-testb", + Rules: []rulefmt.Rule{ + { + Record: "test:record-b1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + }, + }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-testb", + Rules: []rulefmt.Rule{ { - Alert: "testAlertZ1", - Expr: "test-expr-z1", - Labels: map[string]string{"test-label": "z-1"}, - Annotations: map[string]string{"test-annot": "z-1"}, + Record: "test:record-b2", + Expr: "test-expr-b2", + Labels: map[string]string{"test-label": "b-2"}, }, }}, + AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-testb", + Rules: []rulefmt.Rule{ + { + Alert: "testAlertB1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + Annotations: map[string]string{"test-annot": "b-1"}, + }, + }}, + ExtraRules: []model.PromRuleGroup{ + { + Name: "sloth-slo-extra-rules-000-testb", + Interval: 42 * time.Minute, Rules: []rulefmt.Rule{ + { + Alert: "testAlertZ1", + Expr: "test-expr-z1", + Labels: map[string]string{"test-label": "z-1"}, + Annotations: map[string]string{"test-annot": "z-1"}, + }, + }}, {}, // Should be skipped. { - Name: "custom-test-for-extra-rules-zzzzz", + Name: "sloth-slo-extra-rules-001-testb", Rules: []rulefmt.Rule{ { Alert: "testAlertZ2", @@ -247,7 +263,7 @@ groups: expr: test-expr-a2 labels: test-label: a-2 -- name: custom-metadata-name-testa +- name: sloth-slo-meta-recordings-testa rules: - record: test:record-a3 expr: test-expr-a3 @@ -301,7 +317,7 @@ groups: test-label: z-1 annotations: test-annot: z-1 -- name: custom-test-for-extra-rules-zzzzz +- name: sloth-slo-extra-rules-001-testb rules: - alert: testAlertZ2 expr: test-expr-z2 diff --git a/internal/storage/k8s/k8s_test.go b/internal/storage/k8s/k8s_test.go index c46f5053..f9a1280a 100644 --- a/internal/storage/k8s/k8s_test.go +++ b/internal/storage/k8s/k8s_test.go @@ -56,20 +56,22 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { { SLO: model.PromSLO{ID: "testa"}, Rules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-a1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - }, - { - Record: "test:record-a2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - }, - }}, + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-testa", + Rules: []rulefmt.Rule{ + { + Record: "test:record-a1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + }, + { + Record: "test:record-a2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + }, + }}, MetadataRecRules: model.PromRuleGroup{ - Name: "custom-metadata-name-testa", // Custom name. + Name: "sloth-slo-meta-recordings-testa", Rules: []rulefmt.Rule{ { Record: "test:record-a3", @@ -83,6 +85,7 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { }, }}, AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-testa", Interval: 15 * time.Minute, // Custom interval. Rules: []rulefmt.Rule{ { @@ -103,40 +106,49 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { { SLO: model.PromSLO{ID: "testb"}, Rules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-b1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - }, - }}, - MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Record: "test:record-b2", - Expr: "test-expr-b2", - Labels: map[string]string{"test-label": "b-2"}, - }, - }}, - AlertRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ - { - Alert: "testAlertB1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - Annotations: map[string]string{"test-annot": "b-1"}, - }, - }}, - ExtraRules: []model.PromRuleGroup{ - {Interval: 42 * time.Minute, Rules: []rulefmt.Rule{ + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-testb", + Rules: []rulefmt.Rule{ { - Alert: "testAlertZ1", - Expr: "test-expr-z1", - Labels: map[string]string{"test-label": "z-1"}, - Annotations: map[string]string{"test-annot": "z-1"}, + Record: "test:record-b1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, }, }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-testb", + Rules: []rulefmt.Rule{ + { + Record: "test:record-b2", + Expr: "test-expr-b2", + Labels: map[string]string{"test-label": "b-2"}, + }, + }}, + AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-testb", + Rules: []rulefmt.Rule{ + { + Alert: "testAlertB1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + Annotations: map[string]string{"test-annot": "b-1"}, + }, + }}, + ExtraRules: []model.PromRuleGroup{ + { + Name: "sloth-slo-extra-rules-000-testb", + Interval: 42 * time.Minute, + Rules: []rulefmt.Rule{ + { + Alert: "testAlertZ1", + Expr: "test-expr-z1", + Labels: map[string]string{"test-label": "z-1"}, + Annotations: map[string]string{"test-annot": "z-1"}, + }, + }}, {}, // Should be skipped. { - Name: "custom-test-for-extra-rules-zzzzz", + Name: "sloth-slo-extra-rules-001-testb", Rules: []rulefmt.Rule{ { Alert: "testAlertZ2", @@ -198,7 +210,7 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { }, }, { - Name: "custom-metadata-name-testa", + Name: "sloth-slo-meta-recordings-testa", Rules: []monitoringv1.Rule{ { Record: "test:record-a3", @@ -274,7 +286,7 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { }, }, { - Name: "custom-test-for-extra-rules-zzzzz", + Name: "sloth-slo-extra-rules-001-testb", Rules: []monitoringv1.Rule{ { Alert: "testAlertZ2", From 39b835a7b036193049a22061f7260e1d364d346b Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Fri, 24 Oct 2025 19:58:06 +0200 Subject: [PATCH 101/173] Add ability to load plugins in strict mode on sloth lib Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/helpers.go | 21 --------- cmd/sloth/commands/k8scontroller.go | 20 +++++++++ internal/storage/fs/plugin.go | 10 ++++- internal/storage/fs/plugin_test.go | 68 +++++++++++++++++++++++------ pkg/lib/gen.go | 4 +- pkg/lib/helper.go | 4 +- 6 files changed, 87 insertions(+), 40 deletions(-) diff --git a/cmd/sloth/commands/helpers.go b/cmd/sloth/commands/helpers.go index ca4a4dae..d20e538c 100644 --- a/cmd/sloth/commands/helpers.go +++ b/cmd/sloth/commands/helpers.go @@ -5,40 +5,19 @@ import ( "encoding/json" "fmt" "io/fs" - "os" "path/filepath" "regexp" "strings" "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/plugin" plugincorealertrulesv1 "github.com/slok/sloth/internal/plugin/slo/core/alert_rules_v1" plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" plugincorevalidatev1 "github.com/slok/sloth/internal/plugin/slo/core/validate_v1" - pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" - pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" - storagefs "github.com/slok/sloth/internal/storage/fs" "github.com/slok/sloth/pkg/common/model" ) -func createPluginLoader(ctx context.Context, logger log.Logger, paths []string) (*storagefs.FilePluginRepo, error) { - fss := []fs.FS{ - plugin.EmbeddedDefaultSLOPlugins, - } - for _, p := range paths { - fss = append(fss, os.DirFS(p)) - } - - pluginsRepo, err := storagefs.NewFilePluginRepo(logger, pluginenginesli.PluginLoader, pluginengineslo.PluginLoader, fss...) - if err != nil { - return nil, fmt.Errorf("could not create file SLO and SLI plugins repository: %w", err) - } - - return pluginsRepo, nil -} - func discoverSLOManifests(logger log.Logger, exclude, include *regexp.Regexp, path string) ([]string, error) { logger = logger.WithValues(log.Kv{"svc": "SLODiscovery"}) diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index 3ec222c3..9fc908bd 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -33,7 +33,11 @@ import ( "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/app/kubecontroller" "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/internal/plugin" + pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" + pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" "github.com/slok/sloth/internal/storage" + storagefs "github.com/slok/sloth/internal/storage/fs" storageio "github.com/slok/sloth/internal/storage/io" storagek8s "github.com/slok/sloth/internal/storage/k8s" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" @@ -483,3 +487,19 @@ func (g generatorLogger) WithValues(values map[string]interface{}) log.Logger { func (g generatorLogger) WithCtxValues(ctx context.Context) log.Logger { return generatorLogger{Logger: g.Logger.WithCtxValues(ctx)} } + +func createPluginLoader(ctx context.Context, logger log.Logger, paths []string) (*storagefs.FilePluginRepo, error) { + fss := []fs.FS{ + plugin.EmbeddedDefaultSLOPlugins, + } + for _, p := range paths { + fss = append(fss, os.DirFS(p)) + } + + pluginsRepo, err := storagefs.NewFilePluginRepo(logger, false, pluginenginesli.PluginLoader, pluginengineslo.PluginLoader, fss...) + if err != nil { + return nil, fmt.Errorf("could not create file SLO and SLI plugins repository: %w", err) + } + + return pluginsRepo, nil +} diff --git a/internal/storage/fs/plugin.go b/internal/storage/fs/plugin.go index 6b16955d..1c3454b9 100644 --- a/internal/storage/fs/plugin.go +++ b/internal/storage/fs/plugin.go @@ -29,10 +29,11 @@ type FilePluginRepo struct { sliPluginCache map[string]pluginenginesli.SLIPlugin logger log.Logger mu sync.RWMutex + failOnError bool } // NewFilePluginRepo returns a new FilePluginRepo that loads SLI and SLO plugins from the given file system. -func NewFilePluginRepo(logger log.Logger, sliPluginLoader SLIPluginLoader, sloPluginLoader SLOPluginLoader, fss ...fs.FS) (*FilePluginRepo, error) { +func NewFilePluginRepo(logger log.Logger, failOnError bool, sliPluginLoader SLIPluginLoader, sloPluginLoader SLOPluginLoader, fss ...fs.FS) (*FilePluginRepo, error) { r := &FilePluginRepo{ fss: fss, sliPluginLoader: sliPluginLoader, @@ -40,6 +41,7 @@ func NewFilePluginRepo(logger log.Logger, sliPluginLoader SLIPluginLoader, sloPl sloPluginCache: map[string]pluginengineslo.Plugin{}, sliPluginCache: map[string]pluginenginesli.SLIPlugin{}, logger: logger, + failOnError: failOnError, } err := r.Reload(context.Background()) @@ -153,7 +155,11 @@ func (r *FilePluginRepo) loadPlugins(ctx context.Context, fss ...fs.FS) (map[str return nil } - r.logger.Errorf("could not load %q as SLI or SLO plugin: (SLI plugin error: %s | SLO plugin error: %s)", path, sliErr, sloErr) + err = fmt.Errorf("could not load %q as SLI or SLO plugin: (SLI plugin error: %w | SLO plugin error: %w)", path, sliErr, sloErr) + if r.failOnError { + return err + } + r.logger.Errorf(err.Error()) return nil }) diff --git a/internal/storage/fs/plugin_test.go b/internal/storage/fs/plugin_test.go index d8329fd7..b205abf0 100644 --- a/internal/storage/fs/plugin_test.go +++ b/internal/storage/fs/plugin_test.go @@ -19,11 +19,12 @@ import ( func TestFilePluginRepoListSLOPlugins(t *testing.T) { tests := map[string]struct { - fss func() []fs.FS - mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) - expPlugins map[string]pluginengineslo.Plugin - expLoadErr bool - expErr bool + failOnError bool + fss func() []fs.FS + mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) + expPlugins map[string]pluginengineslo.Plugin + expLoadErr bool + expErr bool }{ "Having no files, should return empty list of plugins.": { fss: func() []fs.FS { return nil }, @@ -113,6 +114,26 @@ func TestFilePluginRepoListSLOPlugins(t *testing.T) { "p1": {ID: "p1"}, }, }, + + "Having an error while loading a plugin on strict mode, should fail.": { + failOnError: true, + fss: func() []fs.FS { + m1 := make(fstest.MapFS) + m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + + return []fs.FS{m1} + }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(nil, fmt.Errorf("something")) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + + mslopl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) + mslopl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + + }, + expLoadErr: true, + }, } for name, test := range tests { @@ -124,7 +145,7 @@ func TestFilePluginRepoListSLOPlugins(t *testing.T) { test.mock(mslopl, mslipl) // Create repository and load plugins. - repo, err := storagefs.NewFilePluginRepo(log.Noop, mslipl, mslopl, test.fss()...) + repo, err := storagefs.NewFilePluginRepo(log.Noop, test.failOnError, mslipl, mslopl, test.fss()...) if test.expLoadErr { assert.Error(err) return @@ -185,7 +206,7 @@ func TestFilePluginRepoGetSLOPlugin(t *testing.T) { test.mock(mslopl, mslipl) // Create repository and load plugins. - repo, err := storagefs.NewFilePluginRepo(log.Noop, mslipl, mslopl, test.fss()...) + repo, err := storagefs.NewFilePluginRepo(log.Noop, false, mslipl, mslopl, test.fss()...) require.NoError(err) plugin, err := repo.GetSLOPlugin(t.Context(), test.pluginID) @@ -200,11 +221,12 @@ func TestFilePluginRepoGetSLOPlugin(t *testing.T) { func TestFilePluginRepoListSLIPlugins(t *testing.T) { tests := map[string]struct { - fss func() []fs.FS - mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) - expPlugins map[string]pluginenginesli.SLIPlugin - expLoadErr bool - expErr bool + failOnError bool + fss func() []fs.FS + mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) + expPlugins map[string]pluginenginesli.SLIPlugin + expLoadErr bool + expErr bool }{ "Having no files, should return empty list of plugins.": { fss: func() []fs.FS { return nil }, @@ -281,6 +303,24 @@ func TestFilePluginRepoListSLIPlugins(t *testing.T) { "p1": {ID: "p1"}, }, }, + + "Having an error while loading a plugin on strict mode, should fail.": { + failOnError: true, + fss: func() []fs.FS { + m1 := make(fstest.MapFS) + m1["m1/pl1/plugin.go"] = &fstest.MapFile{Data: []byte("p1")} + m1["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} + + return []fs.FS{m1} + }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) + mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + mslopl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + + }, + expLoadErr: true, + }, } for name, test := range tests { @@ -292,7 +332,7 @@ func TestFilePluginRepoListSLIPlugins(t *testing.T) { test.mock(mslopl, mslipl) // Create repository and load plugins. - repo, err := storagefs.NewFilePluginRepo(log.Noop, mslipl, mslopl, test.fss()...) + repo, err := storagefs.NewFilePluginRepo(log.Noop, test.failOnError, mslipl, mslopl, test.fss()...) if test.expLoadErr { assert.Error(err) return @@ -350,7 +390,7 @@ func TestFilePluginRepoGetSLIPlugin(t *testing.T) { test.mock(mslopl, mslipl) // Create repository and load plugins. - repo, err := storagefs.NewFilePluginRepo(log.Noop, mslipl, mslopl, test.fss()...) + repo, err := storagefs.NewFilePluginRepo(log.Noop, false, mslipl, mslopl, test.fss()...) require.NoError(err) plugin, err := repo.GetSLIPlugin(t.Context(), test.pluginID) diff --git a/pkg/lib/gen.go b/pkg/lib/gen.go index 9b7e6456..e489cc7a 100644 --- a/pkg/lib/gen.go +++ b/pkg/lib/gen.go @@ -40,6 +40,8 @@ type PrometheusSLOGeneratorConfig struct { WindowsFS fs.FS // PluginsFS are the FSs where custom SLO and SLI plugins exist. PluginsFS []fs.FS + // StrictPlugins makes the plugin loader fail when a plugin can't be loaded. + StrictPlugins bool // DefaultSLOPeriod is the default SLO period to use when not specified in the SLO definition. DefaultSLOPeriod time.Duration // DisableDefaultPlugins disables the default SLO plugins, normally used along with custom SLO plugins to fully customize Sloth behavior. @@ -110,7 +112,7 @@ func NewPrometheusSLOGenerator(config PrometheusSLOGeneratorConfig) (*Prometheus } // Create plugin repo. - pluginRepo, err := createPluginLoader(ctx, config.Logger, config.PluginsFS) + pluginRepo, err := createPluginLoader(ctx, config.Logger, config.PluginsFS, config.StrictPlugins) if err != nil { return nil, fmt.Errorf("could not create plugin repository: %w", err) } diff --git a/pkg/lib/helper.go b/pkg/lib/helper.go index a2b70bc9..0ccce2fd 100644 --- a/pkg/lib/helper.go +++ b/pkg/lib/helper.go @@ -17,14 +17,14 @@ import ( storagefs "github.com/slok/sloth/internal/storage/fs" ) -func createPluginLoader(ctx context.Context, logger log.Logger, pluginsFS []fs.FS) (*storagefs.FilePluginRepo, error) { +func createPluginLoader(ctx context.Context, logger log.Logger, pluginsFS []fs.FS, strict bool) (*storagefs.FilePluginRepo, error) { // We should load at least the Sloth embedded default ones. fss := append([]fs.FS{}, pluginsFS...) if len(fss) == 0 { fss = append(fss, plugin.EmbeddedDefaultSLOPlugins) } - pluginsRepo, err := storagefs.NewFilePluginRepo(logger, pluginenginesli.PluginLoader, pluginengineslo.PluginLoader, fss...) + pluginsRepo, err := storagefs.NewFilePluginRepo(logger, strict, pluginenginesli.PluginLoader, pluginengineslo.PluginLoader, fss...) if err != nil { return nil, fmt.Errorf("could not create file SLO and SLI plugins repository: %w", err) } From 809afdd8058e94bba234daa0cb22643dffc362fc Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 25 Oct 2025 11:19:04 +0200 Subject: [PATCH 102/173] Remove unused k8s metadata Signed-off-by: Xabier Larrakoetxea --- internal/storage/storage.go | 5 ++--- pkg/lib/storage.go | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 049eeea4..1b22f341 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -15,7 +15,6 @@ type K8sMeta struct { // SLORulesResult is a common type used to store final SLO rules result in batches. type SLORulesResult struct { - K8sMeta K8sMeta - SLO model.PromSLO - Rules model.PromSLORules + SLO model.PromSLO + Rules model.PromSLORules } diff --git a/pkg/lib/storage.go b/pkg/lib/storage.go index 9ea82367..e9b77a52 100644 --- a/pkg/lib/storage.go +++ b/pkg/lib/storage.go @@ -50,9 +50,8 @@ func WriteResultAsK8sPrometheusOperator(ctx context.Context, k8sMeta K8sMeta, sl storageResults := []storage.SLORulesResult{} for _, rule := range slo.SLOResult { storageResults = append(storageResults, storage.SLORulesResult{ - K8sMeta: kmeta, - SLO: rule.SLO, - Rules: rule.PrometheusRules, + SLO: rule.SLO, + Rules: rule.PrometheusRules, }) } From 0a7cdcaa049a0e10ea7b3f426f5da7d1ae25d9cc Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sun, 26 Oct 2025 11:01:28 +0100 Subject: [PATCH 103/173] Move storage result structs as a common model Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 53 ++-- cmd/sloth/commands/k8scontroller.go | 3 +- internal/app/kubecontroller/handler.go | 15 +- internal/kubernetes/modelmap/slo.go | 33 +-- ...com-prometheus-prometheus-promql-parser.go | 238 +++++++++--------- .../github_com-slok-sloth-pkg-common-model.go | 2 + internal/storage/io/prometheus_operator.go | 3 +- .../storage/io/prometheus_operator_test.go | 34 +-- internal/storage/io/std_prometheus.go | 32 +-- internal/storage/io/std_prometheus_test.go | 34 +-- internal/storage/k8s/dry_run.go | 3 +- internal/storage/k8s/fake.go | 3 +- internal/storage/k8s/k8s.go | 3 +- internal/storage/k8s/k8s_test.go | 16 +- internal/storage/storage.go | 8 - pkg/common/model/gen_result.go | 13 + pkg/lib/gen.go | 32 +-- pkg/lib/lib_as_cli_use_cases_test.go | 38 +-- pkg/lib/storage.go | 26 +- 19 files changed, 279 insertions(+), 310 deletions(-) create mode 100644 pkg/common/model/gen_result.go diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index d91f72b4..81f41f1e 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -262,30 +262,22 @@ func generateSLOs(ctx context.Context, logger log.Logger, genService slothlib.Pr } // Disable data if required. - for i := range genResult.SLOResult { + for i := range genResult.SLOResults { if disableAlerts { - genResult.SLOResult[i].PrometheusRules.AlertRules = model.PromRuleGroup{} + genResult.SLOResults[i].PrometheusRules.AlertRules = model.PromRuleGroup{} } if disableRecordings { - genResult.SLOResult[i].PrometheusRules.SLIErrorRecRules = model.PromRuleGroup{} - genResult.SLOResult[i].PrometheusRules.MetadataRecRules = model.PromRuleGroup{} + genResult.SLOResults[i].PrometheusRules.SLIErrorRecRules = model.PromRuleGroup{} + genResult.SLOResults[i].PrometheusRules.MetadataRecRules = model.PromRuleGroup{} } } // Store results. switch { // Standard prometheus. - case genResult.SLOGroup.OriginalSource.SlothV1 != nil: + case genResult.OriginalSource.SlothV1 != nil: repo := storageio.NewStdPrometheusGroupedRulesYAMLRepo(genTarget.Out, logger) - storageSLOs := make([]storageio.StdPrometheusStorageSLO, 0, len(genResult.SLOResult)) - for _, s := range genResult.SLOResult { - storageSLOs = append(storageSLOs, storageio.StdPrometheusStorageSLO{ - SLO: s.SLO, - Rules: s.PrometheusRules, - }) - } - - err = repo.StoreSLOs(ctx, storageSLOs) + err = repo.StoreSLOs(ctx, *genResult) if err != nil { return fmt.Errorf("could not store SLOS: %w", err) } @@ -293,43 +285,28 @@ func generateSLOs(ctx context.Context, logger log.Logger, genService slothlib.Pr return nil // K8s Sloth CR. - case genResult.SLOGroup.OriginalSource.K8sSlothV1 != nil: + case genResult.OriginalSource.K8sSlothV1 != nil: repo := storageio.NewIOWriterPrometheusOperatorYAMLRepo(genTarget.Out, logger) - storageSLOs := make([]storage.SLORulesResult, 0, len(genResult.SLOResult)) - for _, s := range genResult.SLOResult { - storageSLOs = append(storageSLOs, storage.SLORulesResult{ - SLO: s.SLO, - Rules: s.PrometheusRules, - }) - } kmeta := storage.K8sMeta{ Kind: "PrometheusServiceLevel", APIVersion: "sloth.slok.dev/v1", - UID: string(genResult.SLOGroup.OriginalSource.K8sSlothV1.UID), - Name: genResult.SLOGroup.OriginalSource.K8sSlothV1.Name, - Namespace: genResult.SLOGroup.OriginalSource.K8sSlothV1.Namespace, - Labels: genResult.SLOGroup.OriginalSource.K8sSlothV1.Labels, - Annotations: genResult.SLOGroup.OriginalSource.K8sSlothV1.Annotations, + UID: string(genResult.OriginalSource.K8sSlothV1.UID), + Name: genResult.OriginalSource.K8sSlothV1.Name, + Namespace: genResult.OriginalSource.K8sSlothV1.Namespace, + Labels: genResult.OriginalSource.K8sSlothV1.Labels, + Annotations: genResult.OriginalSource.K8sSlothV1.Annotations, } - err = repo.StoreSLOs(ctx, kmeta, storageSLOs) + err = repo.StoreSLOs(ctx, kmeta, *genResult) if err != nil { return fmt.Errorf("could not store SLOS: %w", err) } // OpenSLO. - case genResult.SLOGroup.OriginalSource.OpenSLOV1Alpha != nil: + case genResult.OriginalSource.OpenSLOV1Alpha != nil: repo := storageio.NewStdPrometheusGroupedRulesYAMLRepo(genTarget.Out, logger) - storageSLOs := make([]storageio.StdPrometheusStorageSLO, 0, len(genResult.SLOResult)) - for _, s := range genResult.SLOResult { - storageSLOs = append(storageSLOs, storageio.StdPrometheusStorageSLO{ - SLO: s.SLO, - Rules: s.PrometheusRules, - }) - } - - err = repo.StoreSLOs(ctx, storageSLOs) + err = repo.StoreSLOs(ctx, *genResult) if err != nil { return fmt.Errorf("could not store SLOS: %w", err) } diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index 9fc908bd..f03c0d27 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -40,6 +40,7 @@ import ( storagefs "github.com/slok/sloth/internal/storage/fs" storageio "github.com/slok/sloth/internal/storage/io" storagek8s "github.com/slok/sloth/internal/storage/k8s" + "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientset "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" ) @@ -387,7 +388,7 @@ type kubernetesService interface { ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error - StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error + StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) error } func (k kubeControllerCommand) newKubernetesService(ctx context.Context, config RootConfig) (kubernetesService, error) { diff --git a/internal/app/kubecontroller/handler.go b/internal/app/kubecontroller/handler.go index 4e57493f..ef736bc8 100644 --- a/internal/app/kubecontroller/handler.go +++ b/internal/app/kubecontroller/handler.go @@ -13,6 +13,7 @@ import ( "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/storage" commonmodel "github.com/slok/sloth/pkg/common/model" + slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" ) @@ -28,7 +29,7 @@ type Generator interface { // Repository knows how to store generated SLO Prometheus rules. type Repository interface { - StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error + StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos commonmodel.PromSLOGroupResult) error } // KubeStatusStorer knows how to set the status of Prometheus service levels Kubernetes CRD. @@ -161,11 +162,13 @@ func (h handler) handlePrometheusServiceLevelV1(ctx context.Context, psl *slothv } // Store on k8s as Prometheus operator Rules. - storageSLOs := make([]storage.SLORulesResult, 0, len(resp.PrometheusSLOs)) + sloResult := commonmodel.PromSLOGroupResult{ + OriginalSource: model.OriginalSource, + } for _, s := range resp.PrometheusSLOs { - storageSLOs = append(storageSLOs, storage.SLORulesResult{ - SLO: s.SLO, - Rules: s.SLORules, + sloResult.SLOResults = append(sloResult.SLOResults, commonmodel.PromSLOResult{ + SLO: s.SLO, + PrometheusRules: s.SLORules, }) } @@ -179,7 +182,7 @@ func (h handler) handlePrometheusServiceLevelV1(ctx context.Context, psl *slothv Annotations: model.OriginalSource.K8sSlothV1.Annotations, } - err = h.repository.StoreSLOs(ctx, kmeta, storageSLOs) + err = h.repository.StoreSLOs(ctx, kmeta, sloResult) if err != nil { return fmt.Errorf("could not store SLOs: %w", err) } diff --git a/internal/kubernetes/modelmap/slo.go b/internal/kubernetes/modelmap/slo.go index 83c27440..70836307 100644 --- a/internal/kubernetes/modelmap/slo.go +++ b/internal/kubernetes/modelmap/slo.go @@ -12,10 +12,11 @@ import ( "github.com/slok/sloth/internal/storage" commonerrors "github.com/slok/sloth/pkg/common/errors" + "github.com/slok/sloth/pkg/common/model" promutils "github.com/slok/sloth/pkg/common/utils/prometheus" ) -func MapModelToPrometheusOperator(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) (*monitoringv1.PrometheusRule, error) { +func MapModelToPrometheusOperator(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) (*monitoringv1.PrometheusRule, error) { // Add extra labels. labels := map[string]string{ "app.kubernetes.io/component": "SLO", @@ -38,37 +39,37 @@ func MapModelToPrometheusOperator(ctx context.Context, kmeta storage.K8sMeta, sl }, } - if len(slos) == 0 { + if len(slos.SLOResults) == 0 { return nil, fmt.Errorf("slo rules required") } - for _, slo := range slos { - if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { + for _, slo := range slos.SLOResults { + if len(slo.PrometheusRules.SLIErrorRecRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Interval: timeDurationToPromOpDuration(slo.Rules.SLIErrorRecRules.Interval), - Name: slo.Rules.SLIErrorRecRules.Name, - Rules: promRulesToKubeRules(slo.Rules.SLIErrorRecRules.Rules), + Interval: timeDurationToPromOpDuration(slo.PrometheusRules.SLIErrorRecRules.Interval), + Name: slo.PrometheusRules.SLIErrorRecRules.Name, + Rules: promRulesToKubeRules(slo.PrometheusRules.SLIErrorRecRules.Rules), }) } - if len(slo.Rules.MetadataRecRules.Rules) > 0 { + if len(slo.PrometheusRules.MetadataRecRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Interval: timeDurationToPromOpDuration(slo.Rules.MetadataRecRules.Interval), - Name: slo.Rules.MetadataRecRules.Name, - Rules: promRulesToKubeRules(slo.Rules.MetadataRecRules.Rules), + Interval: timeDurationToPromOpDuration(slo.PrometheusRules.MetadataRecRules.Interval), + Name: slo.PrometheusRules.MetadataRecRules.Name, + Rules: promRulesToKubeRules(slo.PrometheusRules.MetadataRecRules.Rules), }) } - if len(slo.Rules.AlertRules.Rules) > 0 { + if len(slo.PrometheusRules.AlertRules.Rules) > 0 { rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Interval: timeDurationToPromOpDuration(slo.Rules.AlertRules.Interval), - Name: slo.Rules.AlertRules.Name, - Rules: promRulesToKubeRules(slo.Rules.AlertRules.Rules), + Interval: timeDurationToPromOpDuration(slo.PrometheusRules.AlertRules.Interval), + Name: slo.PrometheusRules.AlertRules.Name, + Rules: promRulesToKubeRules(slo.PrometheusRules.AlertRules.Rules), }) } // Extra rules. - for _, extraRuleGroup := range slo.Rules.ExtraRules { + for _, extraRuleGroup := range slo.PrometheusRules.ExtraRules { if len(extraRuleGroup.Rules) == 0 { continue } diff --git a/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go b/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go index 97165e4b..b7fc1c21 100644 --- a/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go +++ b/internal/pluginengine/slo/custom/github_com-prometheus-prometheus-promql-parser.go @@ -13,123 +13,127 @@ import ( func init() { Symbols["github.com/prometheus/prometheus/promql/parser/parser"] = map[string]reflect.Value{ // function, constant and variable definitions - "ADD": reflect.ValueOf(constant.MakeFromLiteral("57383", token.INT, 0)), - "AT": reflect.ValueOf(constant.MakeFromLiteral("57400", token.INT, 0)), - "ATAN2": reflect.ValueOf(constant.MakeFromLiteral("57401", token.INT, 0)), - "AVG": reflect.ValueOf(constant.MakeFromLiteral("57404", token.INT, 0)), - "BLANK": reflect.ValueOf(constant.MakeFromLiteral("57347", token.INT, 0)), - "BOOL": reflect.ValueOf(constant.MakeFromLiteral("57420", token.INT, 0)), - "BOTTOMK": reflect.ValueOf(constant.MakeFromLiteral("57405", token.INT, 0)), - "BUCKETS_DESC": reflect.ValueOf(constant.MakeFromLiteral("57375", token.INT, 0)), - "BY": reflect.ValueOf(constant.MakeFromLiteral("57421", token.INT, 0)), - "CLOSE_HIST": reflect.ValueOf(constant.MakeFromLiteral("57359", token.INT, 0)), - "COLON": reflect.ValueOf(constant.MakeFromLiteral("57348", token.INT, 0)), - "COMMA": reflect.ValueOf(constant.MakeFromLiteral("57349", token.INT, 0)), - "COMMENT": reflect.ValueOf(constant.MakeFromLiteral("57350", token.INT, 0)), - "COUNT": reflect.ValueOf(constant.MakeFromLiteral("57406", token.INT, 0)), - "COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57436", token.INT, 0)), - "COUNTER_RESET_HINT_DESC": reflect.ValueOf(constant.MakeFromLiteral("57380", token.INT, 0)), - "COUNT_DESC": reflect.ValueOf(constant.MakeFromLiteral("57371", token.INT, 0)), - "COUNT_VALUES": reflect.ValueOf(constant.MakeFromLiteral("57407", token.INT, 0)), - "CUSTOM_VALUES_DESC": reflect.ValueOf(constant.MakeFromLiteral("57379", token.INT, 0)), - "CardManyToMany": reflect.ValueOf(parser.CardManyToMany), - "CardManyToOne": reflect.ValueOf(parser.CardManyToOne), - "CardOneToMany": reflect.ValueOf(parser.CardOneToMany), - "CardOneToOne": reflect.ValueOf(parser.CardOneToOne), - "Children": reflect.ValueOf(parser.Children), - "DIV": reflect.ValueOf(constant.MakeFromLiteral("57384", token.INT, 0)), - "DURATION": reflect.ValueOf(constant.MakeFromLiteral("57351", token.INT, 0)), - "DocumentedType": reflect.ValueOf(parser.DocumentedType), - "END": reflect.ValueOf(constant.MakeFromLiteral("57431", token.INT, 0)), - "EOF": reflect.ValueOf(constant.MakeFromLiteral("57352", token.INT, 0)), - "EQL": reflect.ValueOf(constant.MakeFromLiteral("57346", token.INT, 0)), - "EQLC": reflect.ValueOf(constant.MakeFromLiteral("57385", token.INT, 0)), - "EQL_REGEX": reflect.ValueOf(constant.MakeFromLiteral("57386", token.INT, 0)), - "ERROR": reflect.ValueOf(constant.MakeFromLiteral("57353", token.INT, 0)), - "EnableExperimentalFunctions": reflect.ValueOf(&parser.EnableExperimentalFunctions).Elem(), - "EnrichParseError": reflect.ValueOf(parser.EnrichParseError), - "ExperimentalDurationExpr": reflect.ValueOf(&parser.ExperimentalDurationExpr).Elem(), - "ExtractSelectors": reflect.ValueOf(parser.ExtractSelectors), - "Functions": reflect.ValueOf(&parser.Functions).Elem(), - "GAUGE_TYPE": reflect.ValueOf(constant.MakeFromLiteral("57438", token.INT, 0)), - "GROUP": reflect.ValueOf(constant.MakeFromLiteral("57408", token.INT, 0)), - "GROUP_LEFT": reflect.ValueOf(constant.MakeFromLiteral("57422", token.INT, 0)), - "GROUP_RIGHT": reflect.ValueOf(constant.MakeFromLiteral("57423", token.INT, 0)), - "GTE": reflect.ValueOf(constant.MakeFromLiteral("57387", token.INT, 0)), - "GTR": reflect.ValueOf(constant.MakeFromLiteral("57388", token.INT, 0)), - "IDENTIFIER": reflect.ValueOf(constant.MakeFromLiteral("57354", token.INT, 0)), - "IGNORING": reflect.ValueOf(constant.MakeFromLiteral("57424", token.INT, 0)), - "Inspect": reflect.ValueOf(parser.Inspect), - "ItemTypeStr": reflect.ValueOf(&parser.ItemTypeStr).Elem(), - "LAND": reflect.ValueOf(constant.MakeFromLiteral("57389", token.INT, 0)), - "LEFT_BRACE": reflect.ValueOf(constant.MakeFromLiteral("57355", token.INT, 0)), - "LEFT_BRACKET": reflect.ValueOf(constant.MakeFromLiteral("57356", token.INT, 0)), - "LEFT_PAREN": reflect.ValueOf(constant.MakeFromLiteral("57357", token.INT, 0)), - "LIMITK": reflect.ValueOf(constant.MakeFromLiteral("57416", token.INT, 0)), - "LIMIT_RATIO": reflect.ValueOf(constant.MakeFromLiteral("57417", token.INT, 0)), - "LOR": reflect.ValueOf(constant.MakeFromLiteral("57390", token.INT, 0)), - "LSS": reflect.ValueOf(constant.MakeFromLiteral("57391", token.INT, 0)), - "LTE": reflect.ValueOf(constant.MakeFromLiteral("57392", token.INT, 0)), - "LUNLESS": reflect.ValueOf(constant.MakeFromLiteral("57393", token.INT, 0)), - "Lex": reflect.ValueOf(parser.Lex), - "MAX": reflect.ValueOf(constant.MakeFromLiteral("57409", token.INT, 0)), - "METRIC_IDENTIFIER": reflect.ValueOf(constant.MakeFromLiteral("57360", token.INT, 0)), - "MIN": reflect.ValueOf(constant.MakeFromLiteral("57410", token.INT, 0)), - "MOD": reflect.ValueOf(constant.MakeFromLiteral("57394", token.INT, 0)), - "MUL": reflect.ValueOf(constant.MakeFromLiteral("57395", token.INT, 0)), - "MustGetFunction": reflect.ValueOf(parser.MustGetFunction), - "MustLabelMatcher": reflect.ValueOf(parser.MustLabelMatcher), - "NEGATIVE_BUCKETS_DESC": reflect.ValueOf(constant.MakeFromLiteral("57376", token.INT, 0)), - "NEGATIVE_OFFSET_DESC": reflect.ValueOf(constant.MakeFromLiteral("57374", token.INT, 0)), - "NEQ": reflect.ValueOf(constant.MakeFromLiteral("57396", token.INT, 0)), - "NEQ_REGEX": reflect.ValueOf(constant.MakeFromLiteral("57397", token.INT, 0)), - "NOT_COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57437", token.INT, 0)), - "NUMBER": reflect.ValueOf(constant.MakeFromLiteral("57361", token.INT, 0)), - "NewParser": reflect.ValueOf(parser.NewParser), - "OFFSET": reflect.ValueOf(constant.MakeFromLiteral("57425", token.INT, 0)), - "OFFSET_DESC": reflect.ValueOf(constant.MakeFromLiteral("57373", token.INT, 0)), - "ON": reflect.ValueOf(constant.MakeFromLiteral("57426", token.INT, 0)), - "OPEN_HIST": reflect.ValueOf(constant.MakeFromLiteral("57358", token.INT, 0)), - "POW": reflect.ValueOf(constant.MakeFromLiteral("57398", token.INT, 0)), - "ParseExpr": reflect.ValueOf(parser.ParseExpr), - "ParseMetric": reflect.ValueOf(parser.ParseMetric), - "ParseMetricSelector": reflect.ValueOf(parser.ParseMetricSelector), - "ParseMetricSelectors": reflect.ValueOf(parser.ParseMetricSelectors), - "ParseSeriesDesc": reflect.ValueOf(parser.ParseSeriesDesc), - "Prettify": reflect.ValueOf(parser.Prettify), - "QUANTILE": reflect.ValueOf(constant.MakeFromLiteral("57411", token.INT, 0)), - "RIGHT_BRACE": reflect.ValueOf(constant.MakeFromLiteral("57362", token.INT, 0)), - "RIGHT_BRACKET": reflect.ValueOf(constant.MakeFromLiteral("57363", token.INT, 0)), - "RIGHT_PAREN": reflect.ValueOf(constant.MakeFromLiteral("57364", token.INT, 0)), - "SCHEMA_DESC": reflect.ValueOf(constant.MakeFromLiteral("57372", token.INT, 0)), - "SEMICOLON": reflect.ValueOf(constant.MakeFromLiteral("57365", token.INT, 0)), - "SPACE": reflect.ValueOf(constant.MakeFromLiteral("57366", token.INT, 0)), - "START": reflect.ValueOf(constant.MakeFromLiteral("57430", token.INT, 0)), - "START_EXPRESSION": reflect.ValueOf(constant.MakeFromLiteral("57443", token.INT, 0)), - "START_METRIC": reflect.ValueOf(constant.MakeFromLiteral("57441", token.INT, 0)), - "START_METRIC_SELECTOR": reflect.ValueOf(constant.MakeFromLiteral("57444", token.INT, 0)), - "START_SERIES_DESCRIPTION": reflect.ValueOf(constant.MakeFromLiteral("57442", token.INT, 0)), - "STDDEV": reflect.ValueOf(constant.MakeFromLiteral("57412", token.INT, 0)), - "STDVAR": reflect.ValueOf(constant.MakeFromLiteral("57413", token.INT, 0)), - "STEP": reflect.ValueOf(constant.MakeFromLiteral("57432", token.INT, 0)), - "STRING": reflect.ValueOf(constant.MakeFromLiteral("57367", token.INT, 0)), - "SUB": reflect.ValueOf(constant.MakeFromLiteral("57399", token.INT, 0)), - "SUM": reflect.ValueOf(constant.MakeFromLiteral("57414", token.INT, 0)), - "SUM_DESC": reflect.ValueOf(constant.MakeFromLiteral("57370", token.INT, 0)), - "TIMES": reflect.ValueOf(constant.MakeFromLiteral("57368", token.INT, 0)), - "TOPK": reflect.ValueOf(constant.MakeFromLiteral("57415", token.INT, 0)), - "Tree": reflect.ValueOf(parser.Tree), - "UNKNOWN_COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57435", token.INT, 0)), - "ValueTypeMatrix": reflect.ValueOf(parser.ValueTypeMatrix), - "ValueTypeNone": reflect.ValueOf(parser.ValueTypeNone), - "ValueTypeScalar": reflect.ValueOf(parser.ValueTypeScalar), - "ValueTypeString": reflect.ValueOf(parser.ValueTypeString), - "ValueTypeVector": reflect.ValueOf(parser.ValueTypeVector), - "WITHOUT": reflect.ValueOf(constant.MakeFromLiteral("57427", token.INT, 0)), - "Walk": reflect.ValueOf(parser.Walk), - "WithFunctions": reflect.ValueOf(parser.WithFunctions), - "ZERO_BUCKET_DESC": reflect.ValueOf(constant.MakeFromLiteral("57377", token.INT, 0)), - "ZERO_BUCKET_WIDTH_DESC": reflect.ValueOf(constant.MakeFromLiteral("57378", token.INT, 0)), + "ADD": reflect.ValueOf(constant.MakeFromLiteral("57383", token.INT, 0)), + "ANCHORED": reflect.ValueOf(constant.MakeFromLiteral("57427", token.INT, 0)), + "AT": reflect.ValueOf(constant.MakeFromLiteral("57400", token.INT, 0)), + "ATAN2": reflect.ValueOf(constant.MakeFromLiteral("57401", token.INT, 0)), + "AVG": reflect.ValueOf(constant.MakeFromLiteral("57404", token.INT, 0)), + "BLANK": reflect.ValueOf(constant.MakeFromLiteral("57347", token.INT, 0)), + "BOOL": reflect.ValueOf(constant.MakeFromLiteral("57420", token.INT, 0)), + "BOTTOMK": reflect.ValueOf(constant.MakeFromLiteral("57405", token.INT, 0)), + "BUCKETS_DESC": reflect.ValueOf(constant.MakeFromLiteral("57375", token.INT, 0)), + "BY": reflect.ValueOf(constant.MakeFromLiteral("57421", token.INT, 0)), + "CLOSE_HIST": reflect.ValueOf(constant.MakeFromLiteral("57359", token.INT, 0)), + "COLON": reflect.ValueOf(constant.MakeFromLiteral("57348", token.INT, 0)), + "COMMA": reflect.ValueOf(constant.MakeFromLiteral("57349", token.INT, 0)), + "COMMENT": reflect.ValueOf(constant.MakeFromLiteral("57350", token.INT, 0)), + "COUNT": reflect.ValueOf(constant.MakeFromLiteral("57406", token.INT, 0)), + "COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57438", token.INT, 0)), + "COUNTER_RESET_HINT_DESC": reflect.ValueOf(constant.MakeFromLiteral("57380", token.INT, 0)), + "COUNT_DESC": reflect.ValueOf(constant.MakeFromLiteral("57371", token.INT, 0)), + "COUNT_VALUES": reflect.ValueOf(constant.MakeFromLiteral("57407", token.INT, 0)), + "CUSTOM_VALUES_DESC": reflect.ValueOf(constant.MakeFromLiteral("57379", token.INT, 0)), + "CardManyToMany": reflect.ValueOf(parser.CardManyToMany), + "CardManyToOne": reflect.ValueOf(parser.CardManyToOne), + "CardOneToMany": reflect.ValueOf(parser.CardOneToMany), + "CardOneToOne": reflect.ValueOf(parser.CardOneToOne), + "Children": reflect.ValueOf(parser.Children), + "ChildrenIter": reflect.ValueOf(parser.ChildrenIter), + "DIV": reflect.ValueOf(constant.MakeFromLiteral("57384", token.INT, 0)), + "DURATION": reflect.ValueOf(constant.MakeFromLiteral("57351", token.INT, 0)), + "DocumentedType": reflect.ValueOf(parser.DocumentedType), + "END": reflect.ValueOf(constant.MakeFromLiteral("57433", token.INT, 0)), + "EOF": reflect.ValueOf(constant.MakeFromLiteral("57352", token.INT, 0)), + "EQL": reflect.ValueOf(constant.MakeFromLiteral("57346", token.INT, 0)), + "EQLC": reflect.ValueOf(constant.MakeFromLiteral("57385", token.INT, 0)), + "EQL_REGEX": reflect.ValueOf(constant.MakeFromLiteral("57386", token.INT, 0)), + "ERROR": reflect.ValueOf(constant.MakeFromLiteral("57353", token.INT, 0)), + "EnableExperimentalFunctions": reflect.ValueOf(&parser.EnableExperimentalFunctions).Elem(), + "EnableExtendedRangeSelectors": reflect.ValueOf(&parser.EnableExtendedRangeSelectors).Elem(), + "EnrichParseError": reflect.ValueOf(parser.EnrichParseError), + "ExperimentalDurationExpr": reflect.ValueOf(&parser.ExperimentalDurationExpr).Elem(), + "ExtractSelectors": reflect.ValueOf(parser.ExtractSelectors), + "Functions": reflect.ValueOf(&parser.Functions).Elem(), + "GAUGE_TYPE": reflect.ValueOf(constant.MakeFromLiteral("57440", token.INT, 0)), + "GROUP": reflect.ValueOf(constant.MakeFromLiteral("57408", token.INT, 0)), + "GROUP_LEFT": reflect.ValueOf(constant.MakeFromLiteral("57422", token.INT, 0)), + "GROUP_RIGHT": reflect.ValueOf(constant.MakeFromLiteral("57423", token.INT, 0)), + "GTE": reflect.ValueOf(constant.MakeFromLiteral("57387", token.INT, 0)), + "GTR": reflect.ValueOf(constant.MakeFromLiteral("57388", token.INT, 0)), + "IDENTIFIER": reflect.ValueOf(constant.MakeFromLiteral("57354", token.INT, 0)), + "IGNORING": reflect.ValueOf(constant.MakeFromLiteral("57424", token.INT, 0)), + "Inspect": reflect.ValueOf(parser.Inspect), + "ItemTypeStr": reflect.ValueOf(&parser.ItemTypeStr).Elem(), + "LAND": reflect.ValueOf(constant.MakeFromLiteral("57389", token.INT, 0)), + "LEFT_BRACE": reflect.ValueOf(constant.MakeFromLiteral("57355", token.INT, 0)), + "LEFT_BRACKET": reflect.ValueOf(constant.MakeFromLiteral("57356", token.INT, 0)), + "LEFT_PAREN": reflect.ValueOf(constant.MakeFromLiteral("57357", token.INT, 0)), + "LIMITK": reflect.ValueOf(constant.MakeFromLiteral("57416", token.INT, 0)), + "LIMIT_RATIO": reflect.ValueOf(constant.MakeFromLiteral("57417", token.INT, 0)), + "LOR": reflect.ValueOf(constant.MakeFromLiteral("57390", token.INT, 0)), + "LSS": reflect.ValueOf(constant.MakeFromLiteral("57391", token.INT, 0)), + "LTE": reflect.ValueOf(constant.MakeFromLiteral("57392", token.INT, 0)), + "LUNLESS": reflect.ValueOf(constant.MakeFromLiteral("57393", token.INT, 0)), + "Lex": reflect.ValueOf(parser.Lex), + "MAX": reflect.ValueOf(constant.MakeFromLiteral("57409", token.INT, 0)), + "METRIC_IDENTIFIER": reflect.ValueOf(constant.MakeFromLiteral("57360", token.INT, 0)), + "MIN": reflect.ValueOf(constant.MakeFromLiteral("57410", token.INT, 0)), + "MOD": reflect.ValueOf(constant.MakeFromLiteral("57394", token.INT, 0)), + "MUL": reflect.ValueOf(constant.MakeFromLiteral("57395", token.INT, 0)), + "MustGetFunction": reflect.ValueOf(parser.MustGetFunction), + "MustLabelMatcher": reflect.ValueOf(parser.MustLabelMatcher), + "NEGATIVE_BUCKETS_DESC": reflect.ValueOf(constant.MakeFromLiteral("57376", token.INT, 0)), + "NEGATIVE_OFFSET_DESC": reflect.ValueOf(constant.MakeFromLiteral("57374", token.INT, 0)), + "NEQ": reflect.ValueOf(constant.MakeFromLiteral("57396", token.INT, 0)), + "NEQ_REGEX": reflect.ValueOf(constant.MakeFromLiteral("57397", token.INT, 0)), + "NOT_COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57439", token.INT, 0)), + "NUMBER": reflect.ValueOf(constant.MakeFromLiteral("57361", token.INT, 0)), + "NewParser": reflect.ValueOf(parser.NewParser), + "OFFSET": reflect.ValueOf(constant.MakeFromLiteral("57425", token.INT, 0)), + "OFFSET_DESC": reflect.ValueOf(constant.MakeFromLiteral("57373", token.INT, 0)), + "ON": reflect.ValueOf(constant.MakeFromLiteral("57428", token.INT, 0)), + "OPEN_HIST": reflect.ValueOf(constant.MakeFromLiteral("57358", token.INT, 0)), + "POW": reflect.ValueOf(constant.MakeFromLiteral("57398", token.INT, 0)), + "ParseExpr": reflect.ValueOf(parser.ParseExpr), + "ParseMetric": reflect.ValueOf(parser.ParseMetric), + "ParseMetricSelector": reflect.ValueOf(parser.ParseMetricSelector), + "ParseMetricSelectors": reflect.ValueOf(parser.ParseMetricSelectors), + "ParseSeriesDesc": reflect.ValueOf(parser.ParseSeriesDesc), + "Prettify": reflect.ValueOf(parser.Prettify), + "QUANTILE": reflect.ValueOf(constant.MakeFromLiteral("57411", token.INT, 0)), + "RIGHT_BRACE": reflect.ValueOf(constant.MakeFromLiteral("57362", token.INT, 0)), + "RIGHT_BRACKET": reflect.ValueOf(constant.MakeFromLiteral("57363", token.INT, 0)), + "RIGHT_PAREN": reflect.ValueOf(constant.MakeFromLiteral("57364", token.INT, 0)), + "SCHEMA_DESC": reflect.ValueOf(constant.MakeFromLiteral("57372", token.INT, 0)), + "SEMICOLON": reflect.ValueOf(constant.MakeFromLiteral("57365", token.INT, 0)), + "SMOOTHED": reflect.ValueOf(constant.MakeFromLiteral("57426", token.INT, 0)), + "SPACE": reflect.ValueOf(constant.MakeFromLiteral("57366", token.INT, 0)), + "START": reflect.ValueOf(constant.MakeFromLiteral("57432", token.INT, 0)), + "START_EXPRESSION": reflect.ValueOf(constant.MakeFromLiteral("57445", token.INT, 0)), + "START_METRIC": reflect.ValueOf(constant.MakeFromLiteral("57443", token.INT, 0)), + "START_METRIC_SELECTOR": reflect.ValueOf(constant.MakeFromLiteral("57446", token.INT, 0)), + "START_SERIES_DESCRIPTION": reflect.ValueOf(constant.MakeFromLiteral("57444", token.INT, 0)), + "STDDEV": reflect.ValueOf(constant.MakeFromLiteral("57412", token.INT, 0)), + "STDVAR": reflect.ValueOf(constant.MakeFromLiteral("57413", token.INT, 0)), + "STEP": reflect.ValueOf(constant.MakeFromLiteral("57434", token.INT, 0)), + "STRING": reflect.ValueOf(constant.MakeFromLiteral("57367", token.INT, 0)), + "SUB": reflect.ValueOf(constant.MakeFromLiteral("57399", token.INT, 0)), + "SUM": reflect.ValueOf(constant.MakeFromLiteral("57414", token.INT, 0)), + "SUM_DESC": reflect.ValueOf(constant.MakeFromLiteral("57370", token.INT, 0)), + "TIMES": reflect.ValueOf(constant.MakeFromLiteral("57368", token.INT, 0)), + "TOPK": reflect.ValueOf(constant.MakeFromLiteral("57415", token.INT, 0)), + "Tree": reflect.ValueOf(parser.Tree), + "UNKNOWN_COUNTER_RESET": reflect.ValueOf(constant.MakeFromLiteral("57437", token.INT, 0)), + "ValueTypeMatrix": reflect.ValueOf(parser.ValueTypeMatrix), + "ValueTypeNone": reflect.ValueOf(parser.ValueTypeNone), + "ValueTypeScalar": reflect.ValueOf(parser.ValueTypeScalar), + "ValueTypeString": reflect.ValueOf(parser.ValueTypeString), + "ValueTypeVector": reflect.ValueOf(parser.ValueTypeVector), + "WITHOUT": reflect.ValueOf(constant.MakeFromLiteral("57429", token.INT, 0)), + "Walk": reflect.ValueOf(parser.Walk), + "WithFunctions": reflect.ValueOf(parser.WithFunctions), + "ZERO_BUCKET_DESC": reflect.ValueOf(constant.MakeFromLiteral("57377", token.INT, 0)), + "ZERO_BUCKET_WIDTH_DESC": reflect.ValueOf(constant.MakeFromLiteral("57378", token.INT, 0)), // type definitions "AggregateExpr": reflect.ValueOf((*parser.AggregateExpr)(nil)), diff --git a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go index 8df09429..d012c73d 100644 --- a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go +++ b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go @@ -37,8 +37,10 @@ func init() { "PromSLIRaw": reflect.ValueOf((*model.PromSLIRaw)(nil)), "PromSLO": reflect.ValueOf((*model.PromSLO)(nil)), "PromSLOGroup": reflect.ValueOf((*model.PromSLOGroup)(nil)), + "PromSLOGroupResult": reflect.ValueOf((*model.PromSLOGroupResult)(nil)), "PromSLOGroupSource": reflect.ValueOf((*model.PromSLOGroupSource)(nil)), "PromSLOPluginMetadata": reflect.ValueOf((*model.PromSLOPluginMetadata)(nil)), + "PromSLOResult": reflect.ValueOf((*model.PromSLOResult)(nil)), "PromSLORules": reflect.ValueOf((*model.PromSLORules)(nil)), "SLOPlugins": reflect.ValueOf((*model.SLOPlugins)(nil)), } diff --git a/internal/storage/io/prometheus_operator.go b/internal/storage/io/prometheus_operator.go index 6b8f466f..9c899f85 100644 --- a/internal/storage/io/prometheus_operator.go +++ b/internal/storage/io/prometheus_operator.go @@ -12,6 +12,7 @@ import ( kubernetesmodelmap "github.com/slok/sloth/internal/kubernetes/modelmap" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/storage" + "github.com/slok/sloth/pkg/common/model" ) func NewIOWriterPrometheusOperatorYAMLRepo(writer io.Writer, logger log.Logger) IOWriterPrometheusOperatorYAMLRepo { @@ -30,7 +31,7 @@ type IOWriterPrometheusOperatorYAMLRepo struct { logger log.Logger } -func (i IOWriterPrometheusOperatorYAMLRepo) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error { +func (i IOWriterPrometheusOperatorYAMLRepo) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) error { rule, err := kubernetesmodelmap.MapModelToPrometheusOperator(ctx, kmeta, slos) if err != nil { return fmt.Errorf("could not map model to Prometheus operator CR: %w", err) diff --git a/internal/storage/io/prometheus_operator_test.go b/internal/storage/io/prometheus_operator_test.go index 45372014..c77cbaf9 100644 --- a/internal/storage/io/prometheus_operator_test.go +++ b/internal/storage/io/prometheus_operator_test.go @@ -18,20 +18,20 @@ import ( func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { tests := map[string]struct { k8sMeta storage.K8sMeta - slos []storage.SLORulesResult + slos model.PromSLOGroupResult expYAML string expErr bool }{ "Having 0 SLO rules should fail.": { k8sMeta: storage.K8sMeta{}, - slos: []storage.SLORulesResult{}, + slos: model.PromSLOGroupResult{}, expErr: true, }, "Having 0 SLO rules generated should fail.": { k8sMeta: storage.K8sMeta{}, - slos: []storage.SLORulesResult{ - {}, + slos: model.PromSLOGroupResult{ + SLOResults: []model.PromSLOResult{}, }, expErr: true, }, @@ -43,10 +43,10 @@ func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { Labels: map[string]string{"lk1": "lv1"}, Annotations: map[string]string{"ak1": "av1"}, }, - slos: []storage.SLORulesResult{ + slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ { SLO: model.PromSLO{ID: "test1"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{ Name: "sloth-slo-sli-recordings-test1", Rules: []rulefmt.Rule{ @@ -58,7 +58,7 @@ func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { }}, }, }, - }, + }}, expYAML: ` --- # Code generated by Sloth (dev): https://github.com/slok/sloth. @@ -93,10 +93,10 @@ spec: Labels: map[string]string{"lk1": "lv1"}, Annotations: map[string]string{"ak1": "av1"}, }, - slos: []storage.SLORulesResult{ + slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ { SLO: model.PromSLO{ID: "test1"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ MetadataRecRules: model.PromRuleGroup{ Name: "sloth-slo-meta-recordings-test1", Interval: 42 * time.Minute, @@ -109,7 +109,7 @@ spec: }}, }, }, - }, + }}, expYAML: ` --- # Code generated by Sloth (dev): https://github.com/slok/sloth. @@ -145,10 +145,10 @@ spec: Labels: map[string]string{"lk1": "lv1"}, Annotations: map[string]string{"ak1": "av1"}, }, - slos: []storage.SLORulesResult{ + slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ { SLO: model.PromSLO{ID: "test1"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ AlertRules: model.PromRuleGroup{ Name: "sloth-slo-alerts-test1", Rules: []rulefmt.Rule{ @@ -161,7 +161,7 @@ spec: }}, }, }, - }, + }}, expYAML: ` --- # Code generated by Sloth (dev): https://github.com/slok/sloth. @@ -198,10 +198,10 @@ spec: Labels: map[string]string{"lk1": "lv1"}, Annotations: map[string]string{"ak1": "av1"}, }, - slos: []storage.SLORulesResult{ + slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ { SLO: model.PromSLO{ID: "testa"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{ Name: "sloth-slo-sli-recordings-testa", Rules: []rulefmt.Rule{ @@ -251,7 +251,7 @@ spec: }, { SLO: model.PromSLO{ID: "testb"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{ Name: "sloth-slo-sli-recordings-testb", Rules: []rulefmt.Rule{ @@ -313,7 +313,7 @@ spec: }, }, }, - }, + }}, expYAML: ` --- # Code generated by Sloth (dev): https://github.com/slok/sloth. diff --git a/internal/storage/io/std_prometheus.go b/internal/storage/io/std_prometheus.go index f01c33a0..764c336e 100644 --- a/internal/storage/io/std_prometheus.go +++ b/internal/storage/io/std_prometheus.go @@ -41,39 +41,39 @@ type StdPrometheusStorageSLO struct { // StoreSLOs will store the recording and alert prometheus rules, if grouped is false it will // split and store as 2 different groups the alerts and the recordings, if true // it will be save as a single group. -func (r StdPrometheusGroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos []StdPrometheusStorageSLO) error { - if len(slos) == 0 { +func (r StdPrometheusGroupedRulesYAMLRepo) StoreSLOs(ctx context.Context, slos model.PromSLOGroupResult) error { + if len(slos.SLOResults) == 0 { return fmt.Errorf("slo rules required") } ruleGroups := stdPromRuleGroupsYAMLv2{} - for _, slo := range slos { - if len(slo.Rules.SLIErrorRecRules.Rules) > 0 { + for _, slo := range slos.SLOResults { + if len(slo.PrometheusRules.SLIErrorRecRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ - Interval: prommodel.Duration(slo.Rules.SLIErrorRecRules.Interval), - Name: slo.Rules.SLIErrorRecRules.Name, - Rules: slo.Rules.SLIErrorRecRules.Rules, + Interval: prommodel.Duration(slo.PrometheusRules.SLIErrorRecRules.Interval), + Name: slo.PrometheusRules.SLIErrorRecRules.Name, + Rules: slo.PrometheusRules.SLIErrorRecRules.Rules, }) } - if len(slo.Rules.MetadataRecRules.Rules) > 0 { + if len(slo.PrometheusRules.MetadataRecRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ - Interval: prommodel.Duration(slo.Rules.MetadataRecRules.Interval), - Name: slo.Rules.MetadataRecRules.Name, - Rules: slo.Rules.MetadataRecRules.Rules, + Interval: prommodel.Duration(slo.PrometheusRules.MetadataRecRules.Interval), + Name: slo.PrometheusRules.MetadataRecRules.Name, + Rules: slo.PrometheusRules.MetadataRecRules.Rules, }) } - if len(slo.Rules.AlertRules.Rules) > 0 { + if len(slo.PrometheusRules.AlertRules.Rules) > 0 { ruleGroups.Groups = append(ruleGroups.Groups, stdPromRuleGroupYAMLv2{ - Interval: prommodel.Duration(slo.Rules.AlertRules.Interval), - Name: slo.Rules.AlertRules.Name, - Rules: slo.Rules.AlertRules.Rules, + Interval: prommodel.Duration(slo.PrometheusRules.AlertRules.Interval), + Name: slo.PrometheusRules.AlertRules.Name, + Rules: slo.PrometheusRules.AlertRules.Rules, }) } // Extra rules. - for _, extraRuleGroup := range slo.Rules.ExtraRules { + for _, extraRuleGroup := range slo.PrometheusRules.ExtraRules { if len(extraRuleGroup.Rules) == 0 { continue } diff --git a/internal/storage/io/std_prometheus_test.go b/internal/storage/io/std_prometheus_test.go index b1325c5c..1a04a42c 100644 --- a/internal/storage/io/std_prometheus_test.go +++ b/internal/storage/io/std_prometheus_test.go @@ -16,27 +16,27 @@ import ( func TestGroupedRulesYAMLRepoStore(t *testing.T) { tests := map[string]struct { - slos []io.StdPrometheusStorageSLO + slos model.PromSLOGroupResult expYAML string expErr bool }{ "Having 0 SLO rules should fail.": { - slos: []io.StdPrometheusStorageSLO{}, + slos: model.PromSLOGroupResult{}, expErr: true, }, "Having 0 SLO rules generated should fail.": { - slos: []io.StdPrometheusStorageSLO{ - {}, + slos: model.PromSLOGroupResult{ + SLOResults: []model.PromSLOResult{}, }, expErr: true, }, "Having a single SLI recording rule should render correctly.": { - slos: []io.StdPrometheusStorageSLO{ + slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ { SLO: model.PromSLO{ID: "test1"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{ Name: "sloth-slo-sli-recordings-test1", Rules: []rulefmt.Rule{ @@ -48,7 +48,7 @@ func TestGroupedRulesYAMLRepoStore(t *testing.T) { }}, }, }, - }, + }}, expYAML: ` --- # Code generated by Sloth (dev): https://github.com/slok/sloth. @@ -64,10 +64,10 @@ groups: `, }, "Having a single metadata recording rule should render correctly.": { - slos: []io.StdPrometheusStorageSLO{ + slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ { SLO: model.PromSLO{ID: "test1"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ MetadataRecRules: model.PromRuleGroup{ Name: "sloth-slo-meta-recordings-test1", Rules: []rulefmt.Rule{ @@ -79,7 +79,7 @@ groups: }}, }, }, - }, + }}, expYAML: ` --- # Code generated by Sloth (dev): https://github.com/slok/sloth. @@ -95,10 +95,10 @@ groups: `, }, "Having a single SLO alert rule should render correctly.": { - slos: []io.StdPrometheusStorageSLO{ + slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ { SLO: model.PromSLO{ID: "test1"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ AlertRules: model.PromRuleGroup{ Name: "sloth-slo-alerts-test1", Interval: 42 * time.Minute, @@ -112,7 +112,7 @@ groups: }}, }, }, - }, + }}, expYAML: ` --- # Code generated by Sloth (dev): https://github.com/slok/sloth. @@ -132,10 +132,10 @@ groups: }, "Having a mixed example of multiple rules and options should render correctly.": { - slos: []io.StdPrometheusStorageSLO{ + slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ { SLO: model.PromSLO{ID: "testa"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{ Name: "sloth-slo-sli-recordings-testa", Rules: []rulefmt.Rule{ @@ -185,7 +185,7 @@ groups: }, { SLO: model.PromSLO{ID: "testb"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{ Name: "sloth-slo-sli-recordings-testb", Rules: []rulefmt.Rule{ @@ -246,7 +246,7 @@ groups: }, }, }, - }, + }}, expYAML: ` --- # Code generated by Sloth (dev): https://github.com/slok/sloth. diff --git a/internal/storage/k8s/dry_run.go b/internal/storage/k8s/dry_run.go index 2a03216f..967a52b2 100644 --- a/internal/storage/k8s/dry_run.go +++ b/internal/storage/k8s/dry_run.go @@ -8,6 +8,7 @@ import ( "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/storage" + "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" ) @@ -37,7 +38,7 @@ func (r DryRunApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx contex return nil } -func (r DryRunApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error { +func (r DryRunApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) error { r.logger.Infof("Dry run StoreSLOs") return nil } diff --git a/internal/storage/k8s/fake.go b/internal/storage/k8s/fake.go index a65066de..9f7c0822 100644 --- a/internal/storage/k8s/fake.go +++ b/internal/storage/k8s/fake.go @@ -10,6 +10,7 @@ import ( "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/storage" + "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientsetfake "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/fake" ) @@ -41,7 +42,7 @@ func (r FakeApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context. return r.ksvc.EnsurePrometheusServiceLevelStatus(ctx, slo, err) } -func (r FakeApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error { +func (r FakeApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) error { return r.ksvc.StoreSLOs(ctx, kmeta, slos) } diff --git a/internal/storage/k8s/k8s.go b/internal/storage/k8s/k8s.go index 6e4affb6..8656ae5a 100644 --- a/internal/storage/k8s/k8s.go +++ b/internal/storage/k8s/k8s.go @@ -15,6 +15,7 @@ import ( kubernetesmodelmap "github.com/slok/sloth/internal/kubernetes/modelmap" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/storage" + "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientset "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" ) @@ -64,7 +65,7 @@ func (r ApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Cont return err } -func (r ApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos []storage.SLORulesResult) error { +func (r ApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) error { // Map to the Prometheus operator CRD. rule, err := kubernetesmodelmap.MapModelToPrometheusOperator(ctx, kmeta, slos) if err != nil { diff --git a/internal/storage/k8s/k8s_test.go b/internal/storage/k8s/k8s_test.go index f9a1280a..23e97bcf 100644 --- a/internal/storage/k8s/k8s_test.go +++ b/internal/storage/k8s/k8s_test.go @@ -24,20 +24,20 @@ import ( func TestApiserverRepositoryStoreSLOs(t *testing.T) { tests := map[string]struct { k8sMeta storage.K8sMeta - slos []storage.SLORulesResult + slos model.PromSLOGroupResult expPromOperatorRules []monitoringv1.PrometheusRule expErr bool }{ "Having 0 SLO rules should fail.": { k8sMeta: storage.K8sMeta{}, - slos: []storage.SLORulesResult{}, + slos: model.PromSLOGroupResult{}, expErr: true, }, "Having 0 SLO rules generated should fail.": { k8sMeta: storage.K8sMeta{}, - slos: []storage.SLORulesResult{ - {}, + slos: model.PromSLOGroupResult{ + SLOResults: []model.PromSLOResult{}, }, expErr: true, }, @@ -52,10 +52,10 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { APIVersion: "test-apiversion", UID: "test-uid", }, - slos: []storage.SLORulesResult{ + slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ { SLO: model.PromSLO{ID: "testa"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{ Name: "sloth-slo-sli-recordings-testa", Rules: []rulefmt.Rule{ @@ -105,7 +105,7 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { }, { SLO: model.PromSLO{ID: "testb"}, - Rules: model.PromSLORules{ + PrometheusRules: model.PromSLORules{ SLIErrorRecRules: model.PromRuleGroup{ Name: "sloth-slo-sli-recordings-testb", Rules: []rulefmt.Rule{ @@ -167,7 +167,7 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { }, }, }, - }, + }}, expPromOperatorRules: []monitoringv1.PrometheusRule{ { TypeMeta: metav1.TypeMeta{ diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 1b22f341..529bfec6 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -1,7 +1,5 @@ package storage -import "github.com/slok/sloth/pkg/common/model" - // K8sMeta is the Kubernetes metadata simplified used for storage purposes. type K8sMeta struct { Kind string `validate:"required"` @@ -12,9 +10,3 @@ type K8sMeta struct { Annotations map[string]string Labels map[string]string } - -// SLORulesResult is a common type used to store final SLO rules result in batches. -type SLORulesResult struct { - SLO model.PromSLO - Rules model.PromSLORules -} diff --git a/pkg/common/model/gen_result.go b/pkg/common/model/gen_result.go new file mode 100644 index 00000000..2c35493b --- /dev/null +++ b/pkg/common/model/gen_result.go @@ -0,0 +1,13 @@ +package model + +// PromSLOGroupResult is the result of generating standard Prometheus SLO rules from SLO definitions as SLO group. +type PromSLOGroupResult struct { + OriginalSource PromSLOGroupSource + SLOResults []PromSLOResult +} + +// PromSLOResult is the result of generating standard Prometheus SLO rules from SLO definitions. +type PromSLOResult struct { + SLO PromSLO + PrometheusRules PromSLORules +} diff --git a/pkg/lib/gen.go b/pkg/lib/gen.go index e489cc7a..90d74330 100644 --- a/pkg/lib/gen.go +++ b/pkg/lib/gen.go @@ -148,22 +148,10 @@ func NewPrometheusSLOGenerator(config PrometheusSLOGeneratorConfig) (*Prometheus }, nil } -// SLOGroupPrometheusStdResult is the result of generating standard Prometheus SLO rules from SLO definitions as SLO group. -type SLOGroupPrometheusStdResult struct { - SLOGroup model.PromSLOGroup - SLOResult []SLOPrometheusStdResult -} - -// SLOPrometheusStdResult is the result of generating standard Prometheus SLO rules from SLO definitions. -type SLOPrometheusStdResult struct { - SLO model.PromSLO - PrometheusRules model.PromSLORules -} - // GenerateFromRaw generates SLO rules from raw data, it will infer what type of SLO spec receives. This method is the most // generic one as the user doesn't need to know what type of SLO spec is using. // For more custom programmatic usage use the other Go struct API spec generators. -func (p PrometheusSLOGenerator) GenerateFromRaw(ctx context.Context, data []byte) (*SLOGroupPrometheusStdResult, error) { +func (p PrometheusSLOGenerator) GenerateFromRaw(ctx context.Context, data []byte) (*model.PromSLOGroupResult, error) { // For now we only support yaml specs, so this is safe to do. yamlData := utilsdata.SplitYAML(data) if len(yamlData) > 1 { @@ -202,7 +190,7 @@ func (p PrometheusSLOGenerator) GenerateFromRaw(ctx context.Context, data []byte } // GenerateFromSlothV1 generates SLOs from a Sloth Prometheus SLO definition spec struct. -func (p PrometheusSLOGenerator) GenerateFromSlothV1(ctx context.Context, spec prometheusv1.Spec) (*SLOGroupPrometheusStdResult, error) { +func (p PrometheusSLOGenerator) GenerateFromSlothV1(ctx context.Context, spec prometheusv1.Spec) (*model.PromSLOGroupResult, error) { spec.Version = prometheusv1.Version // Force version in case is missing(we already know what it is with the type). sloGroup, err := p.promYAMLLoader.MapSpecToModel(ctx, spec) @@ -233,7 +221,7 @@ func (p PrometheusSLOGenerator) GenerateFromSlothV1(ctx context.Context, spec pr } // GenerateFromK8sV1 generates SLO rules from a Kubernetes Sloth CR SLO definition spec struct. -func (p PrometheusSLOGenerator) GenerateFromK8sV1(ctx context.Context, spec kubernetesv1.PrometheusServiceLevel) (*SLOGroupPrometheusStdResult, error) { +func (p PrometheusSLOGenerator) GenerateFromK8sV1(ctx context.Context, spec kubernetesv1.PrometheusServiceLevel) (*model.PromSLOGroupResult, error) { sloGroup, err := p.kubeYAMLLoader.MapSpecToModel(ctx, spec) if err != nil { return nil, fmt.Errorf("could not map to model: %w", err) @@ -262,7 +250,7 @@ func (p PrometheusSLOGenerator) GenerateFromK8sV1(ctx context.Context, spec kube } // GenerateFromOpenSLOV1Alpha generates SLO rules from an OpenSLO SLO definition spec struct. -func (p PrometheusSLOGenerator) GenerateFromOpenSLOV1Alpha(ctx context.Context, spec openslov1alpha.SLO) (*SLOGroupPrometheusStdResult, error) { +func (p PrometheusSLOGenerator) GenerateFromOpenSLOV1Alpha(ctx context.Context, spec openslov1alpha.SLO) (*model.PromSLOGroupResult, error) { sloGroup, err := p.openSLOYAMLLoader.MapSpecToModel(spec) if err != nil { return nil, fmt.Errorf("could not map to model: %w", err) @@ -290,22 +278,22 @@ func (p PrometheusSLOGenerator) GenerateFromOpenSLOV1Alpha(ctx context.Context, return p.generateFromModel(ctx, req) } -func (p PrometheusSLOGenerator) generateFromModel(ctx context.Context, req generate.Request) (*SLOGroupPrometheusStdResult, error) { +func (p PrometheusSLOGenerator) generateFromModel(ctx context.Context, req generate.Request) (*model.PromSLOGroupResult, error) { res, err := p.genSvc.Generate(ctx, req) if err != nil { return nil, fmt.Errorf("could not generate Prometheus SLO rules: %w", err) } - result := []SLOPrometheusStdResult{} + result := []model.PromSLOResult{} for _, r := range res.PrometheusSLOs { - result = append(result, SLOPrometheusStdResult{ + result = append(result, model.PromSLOResult{ SLO: r.SLO, PrometheusRules: r.SLORules, }) } - return &SLOGroupPrometheusStdResult{ - SLOGroup: req.SLOGroup, - SLOResult: result, + return &model.PromSLOGroupResult{ + OriginalSource: req.SLOGroup.OriginalSource, + SLOResults: result, }, nil } diff --git a/pkg/lib/lib_as_cli_use_cases_test.go b/pkg/lib/lib_as_cli_use_cases_test.go index 4593f231..a040317e 100644 --- a/pkg/lib/lib_as_cli_use_cases_test.go +++ b/pkg/lib/lib_as_cli_use_cases_test.go @@ -25,7 +25,7 @@ func TestLibAsCLIIntegration(t *testing.T) { tests := map[string]struct { config func() lib.PrometheusSLOGeneratorConfig inFilePath string - resultFormatter func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte + resultFormatter func(t *testing.T, result model.PromSLOGroupResult) []byte expOutFilePath string expGenErr bool }{ @@ -43,7 +43,7 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { var b bytes.Buffer err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) @@ -57,7 +57,7 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base-k8s.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-k8s.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { var b bytes.Buffer kmeta := lib.K8sMeta{Name: "svc", Namespace: "test-ns"} err := lib.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) @@ -72,7 +72,7 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-openslo.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-openslo.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { var b bytes.Buffer err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) @@ -89,7 +89,7 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-28d.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { var b bytes.Buffer err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) @@ -107,7 +107,7 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-custom-windows-7d.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { var b bytes.Buffer err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) @@ -124,7 +124,7 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-extra-labels.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { var b bytes.Buffer err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) @@ -138,10 +138,10 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-no-alerts.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { - for i := range result.SLOResult { - result.SLOResult[i].PrometheusRules.AlertRules = model.PromRuleGroup{} + for i := range result.SLOResults { + result.SLOResults[i].PrometheusRules.AlertRules = model.PromRuleGroup{} } var b bytes.Buffer @@ -157,11 +157,11 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-no-recordings.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { // Remove alerts. - for i := range result.SLOResult { - result.SLOResult[i].PrometheusRules.SLIErrorRecRules = model.PromRuleGroup{} - result.SLOResult[i].PrometheusRules.MetadataRecRules = model.PromRuleGroup{} + for i := range result.SLOResults { + result.SLOResults[i].PrometheusRules.SLIErrorRecRules = model.PromRuleGroup{} + result.SLOResults[i].PrometheusRules.MetadataRecRules = model.PromRuleGroup{} } var b bytes.Buffer @@ -180,7 +180,7 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-sli-plugin.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-sli-plugin.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { var b bytes.Buffer err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) @@ -197,7 +197,7 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-slo-plugin.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-slo-plugin.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { var b bytes.Buffer err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) @@ -214,7 +214,7 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-slo-plugin-k8s.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { var b bytes.Buffer kmeta := lib.K8sMeta{Name: "svc", Namespace: "test-ns"} err := lib.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) @@ -228,7 +228,7 @@ func TestLibAsCLIIntegration(t *testing.T) { return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} }, inFilePath: "../../test/integration/prometheus/testdata/in-multifile.yaml", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { return nil }, + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { return nil }, expGenErr: true, }, @@ -237,7 +237,7 @@ func TestLibAsCLIIntegration(t *testing.T) { return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} }, inFilePath: "../../test/integration/prometheus/testdata/in-multifile-k8s.yaml", - resultFormatter: func(t *testing.T, result lib.SLOGroupPrometheusStdResult) []byte { return nil }, + resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { return nil }, expGenErr: true, }, } diff --git a/pkg/lib/storage.go b/pkg/lib/storage.go index e9b77a52..1bae6f0c 100644 --- a/pkg/lib/storage.go +++ b/pkg/lib/storage.go @@ -6,6 +6,7 @@ import ( "github.com/slok/sloth/internal/storage" storageio "github.com/slok/sloth/internal/storage/io" + "github.com/slok/sloth/pkg/common/model" "github.com/slok/sloth/pkg/lib/log" ) @@ -13,18 +14,9 @@ import ( // More information in: // - https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/#recording-rules. // - https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/. -func WriteResultAsPrometheusStd(ctx context.Context, slo SLOGroupPrometheusStdResult, w io.Writer) error { +func WriteResultAsPrometheusStd(ctx context.Context, slo model.PromSLOGroupResult, w io.Writer) error { repo := storageio.NewStdPrometheusGroupedRulesYAMLRepo(w, log.Noop) - - storageResults := []storageio.StdPrometheusStorageSLO{} - for _, rule := range slo.SLOResult { - storageResults = append(storageResults, storageio.StdPrometheusStorageSLO{ - SLO: rule.SLO, - Rules: rule.PrometheusRules, - }) - } - - return repo.StoreSLOs(ctx, storageResults) + return repo.StoreSLOs(ctx, slo) } // K8sMeta is the Kubernetes metadata to use when writing Kubernetes related rules. @@ -37,7 +29,7 @@ type K8sMeta struct { // WriteResultAsK8sPrometheusOperator writes the SLO results into the writer as a Prometheus Operator CRD file. // More information in: https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.PrometheusRule. -func WriteResultAsK8sPrometheusOperator(ctx context.Context, k8sMeta K8sMeta, slo SLOGroupPrometheusStdResult, w io.Writer) error { +func WriteResultAsK8sPrometheusOperator(ctx context.Context, k8sMeta K8sMeta, slo model.PromSLOGroupResult, w io.Writer) error { repo := storageio.NewIOWriterPrometheusOperatorYAMLRepo(w, log.Noop) kmeta := storage.K8sMeta{ @@ -47,13 +39,5 @@ func WriteResultAsK8sPrometheusOperator(ctx context.Context, k8sMeta K8sMeta, sl Labels: k8sMeta.Labels, } - storageResults := []storage.SLORulesResult{} - for _, rule := range slo.SLOResult { - storageResults = append(storageResults, storage.SLORulesResult{ - SLO: rule.SLO, - Rules: rule.PrometheusRules, - }) - } - - return repo.StoreSLOs(ctx, kmeta, storageResults) + return repo.StoreSLOs(ctx, kmeta, slo) } From fbdcb4550e1fd304c06b202563440eeab0a6d99d Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sun, 26 Oct 2025 12:26:50 +0100 Subject: [PATCH 104/173] Move k8s meta storage structs as a common model Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 6 +- cmd/sloth/commands/k8scontroller.go | 3 +- internal/app/kubecontroller/handler.go | 8 +- internal/kubernetes/modelmap/slo.go | 3 +- .../github_com-slok-sloth-pkg-common-model.go | 1 + internal/storage/io/prometheus_operator.go | 3 +- .../storage/io/prometheus_operator_test.go | 15 +- internal/storage/k8s/dry_run.go | 3 +- internal/storage/k8s/fake.go | 3 +- internal/storage/k8s/k8s.go | 12 +- internal/storage/k8s/k8s_test.go | 228 +++++++++--------- internal/storage/storage.go | 12 - pkg/common/model/k8s.go | 9 + pkg/lib/lib_as_cli_use_cases_test.go | 4 +- pkg/lib/lib_test.go | 3 +- pkg/lib/storage.go | 21 +- 16 files changed, 153 insertions(+), 181 deletions(-) delete mode 100644 internal/storage/storage.go create mode 100644 pkg/common/model/k8s.go diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 81f41f1e..19a077fb 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -17,7 +17,6 @@ import ( "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/plugin" - "github.com/slok/sloth/internal/storage" storageio "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" @@ -288,10 +287,7 @@ func generateSLOs(ctx context.Context, logger log.Logger, genService slothlib.Pr case genResult.OriginalSource.K8sSlothV1 != nil: repo := storageio.NewIOWriterPrometheusOperatorYAMLRepo(genTarget.Out, logger) - kmeta := storage.K8sMeta{ - Kind: "PrometheusServiceLevel", - APIVersion: "sloth.slok.dev/v1", - UID: string(genResult.OriginalSource.K8sSlothV1.UID), + kmeta := model.K8sMeta{ Name: genResult.OriginalSource.K8sSlothV1.Name, Namespace: genResult.OriginalSource.K8sSlothV1.Namespace, Labels: genResult.OriginalSource.K8sSlothV1.Labels, diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index f03c0d27..55a141ff 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -36,7 +36,6 @@ import ( "github.com/slok/sloth/internal/plugin" pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" - "github.com/slok/sloth/internal/storage" storagefs "github.com/slok/sloth/internal/storage/fs" storageio "github.com/slok/sloth/internal/storage/io" storagek8s "github.com/slok/sloth/internal/storage/k8s" @@ -388,7 +387,7 @@ type kubernetesService interface { ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) WatchPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (watch.Interface, error) EnsurePrometheusServiceLevelStatus(ctx context.Context, slo *slothv1.PrometheusServiceLevel, err error) error - StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) error + StoreSLOs(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) error } func (k kubeControllerCommand) newKubernetesService(ctx context.Context, config RootConfig) (kubernetesService, error) { diff --git a/internal/app/kubecontroller/handler.go b/internal/app/kubecontroller/handler.go index ef736bc8..febb879f 100644 --- a/internal/app/kubecontroller/handler.go +++ b/internal/app/kubecontroller/handler.go @@ -11,7 +11,6 @@ import ( "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/info" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/storage" commonmodel "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" @@ -29,7 +28,7 @@ type Generator interface { // Repository knows how to store generated SLO Prometheus rules. type Repository interface { - StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos commonmodel.PromSLOGroupResult) error + StoreSLOs(ctx context.Context, kmeta commonmodel.K8sMeta, slos commonmodel.PromSLOGroupResult) error } // KubeStatusStorer knows how to set the status of Prometheus service levels Kubernetes CRD. @@ -172,10 +171,7 @@ func (h handler) handlePrometheusServiceLevelV1(ctx context.Context, psl *slothv }) } - kmeta := storage.K8sMeta{ - Kind: "PrometheusServiceLevel", - APIVersion: "sloth.slok.dev/v1", - UID: string(model.OriginalSource.K8sSlothV1.UID), + kmeta := commonmodel.K8sMeta{ Name: model.OriginalSource.K8sSlothV1.Name, Namespace: model.OriginalSource.K8sSlothV1.Namespace, Labels: model.OriginalSource.K8sSlothV1.Labels, diff --git a/internal/kubernetes/modelmap/slo.go b/internal/kubernetes/modelmap/slo.go index 70836307..c8aa510c 100644 --- a/internal/kubernetes/modelmap/slo.go +++ b/internal/kubernetes/modelmap/slo.go @@ -10,13 +10,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" - "github.com/slok/sloth/internal/storage" commonerrors "github.com/slok/sloth/pkg/common/errors" "github.com/slok/sloth/pkg/common/model" promutils "github.com/slok/sloth/pkg/common/utils/prometheus" ) -func MapModelToPrometheusOperator(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) (*monitoringv1.PrometheusRule, error) { +func MapModelToPrometheusOperator(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) (*monitoringv1.PrometheusRule, error) { // Add extra labels. labels := map[string]string{ "app.kubernetes.io/component": "SLO", diff --git a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go index d012c73d..83fabeaa 100644 --- a/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go +++ b/internal/pluginengine/slo/custom/github_com-slok-sloth-pkg-common-model.go @@ -27,6 +27,7 @@ func init() { // type definitions "AlertSeverity": reflect.ValueOf((*model.AlertSeverity)(nil)), "Info": reflect.ValueOf((*model.Info)(nil)), + "K8sMeta": reflect.ValueOf((*model.K8sMeta)(nil)), "MWMBAlert": reflect.ValueOf((*model.MWMBAlert)(nil)), "MWMBAlertGroup": reflect.ValueOf((*model.MWMBAlertGroup)(nil)), "Mode": reflect.ValueOf((*model.Mode)(nil)), diff --git a/internal/storage/io/prometheus_operator.go b/internal/storage/io/prometheus_operator.go index 9c899f85..903b3660 100644 --- a/internal/storage/io/prometheus_operator.go +++ b/internal/storage/io/prometheus_operator.go @@ -11,7 +11,6 @@ import ( kubernetesmodelmap "github.com/slok/sloth/internal/kubernetes/modelmap" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/storage" "github.com/slok/sloth/pkg/common/model" ) @@ -31,7 +30,7 @@ type IOWriterPrometheusOperatorYAMLRepo struct { logger log.Logger } -func (i IOWriterPrometheusOperatorYAMLRepo) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) error { +func (i IOWriterPrometheusOperatorYAMLRepo) StoreSLOs(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) error { rule, err := kubernetesmodelmap.MapModelToPrometheusOperator(ctx, kmeta, slos) if err != nil { return fmt.Errorf("could not map model to Prometheus operator CR: %w", err) diff --git a/internal/storage/io/prometheus_operator_test.go b/internal/storage/io/prometheus_operator_test.go index c77cbaf9..09d9253a 100644 --- a/internal/storage/io/prometheus_operator_test.go +++ b/internal/storage/io/prometheus_operator_test.go @@ -10,26 +10,25 @@ import ( "github.com/stretchr/testify/assert" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/storage" "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" ) func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { tests := map[string]struct { - k8sMeta storage.K8sMeta + k8sMeta model.K8sMeta slos model.PromSLOGroupResult expYAML string expErr bool }{ "Having 0 SLO rules should fail.": { - k8sMeta: storage.K8sMeta{}, + k8sMeta: model.K8sMeta{}, slos: model.PromSLOGroupResult{}, expErr: true, }, "Having 0 SLO rules generated should fail.": { - k8sMeta: storage.K8sMeta{}, + k8sMeta: model.K8sMeta{}, slos: model.PromSLOGroupResult{ SLOResults: []model.PromSLOResult{}, }, @@ -37,7 +36,7 @@ func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { }, "Having a single SLI recording rule should render correctly.": { - k8sMeta: storage.K8sMeta{ + k8sMeta: model.K8sMeta{ Name: "test-name", Namespace: "test-ns", Labels: map[string]string{"lk1": "lv1"}, @@ -87,7 +86,7 @@ spec: }, "Having a single metadata recording rule should render correctly.": { - k8sMeta: storage.K8sMeta{ + k8sMeta: model.K8sMeta{ Name: "test-name", Namespace: "test-ns", Labels: map[string]string{"lk1": "lv1"}, @@ -139,7 +138,7 @@ spec: }, "Having a single SLO alert rule should render correctly.": { - k8sMeta: storage.K8sMeta{ + k8sMeta: model.K8sMeta{ Name: "test-name", Namespace: "test-ns", Labels: map[string]string{"lk1": "lv1"}, @@ -192,7 +191,7 @@ spec: }, "Having a multiple SLO alert and recording rules should render correctly.": { - k8sMeta: storage.K8sMeta{ + k8sMeta: model.K8sMeta{ Name: "test-name", Namespace: "test-ns", Labels: map[string]string{"lk1": "lv1"}, diff --git a/internal/storage/k8s/dry_run.go b/internal/storage/k8s/dry_run.go index 967a52b2..33e37471 100644 --- a/internal/storage/k8s/dry_run.go +++ b/internal/storage/k8s/dry_run.go @@ -7,7 +7,6 @@ import ( "k8s.io/apimachinery/pkg/watch" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/storage" "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" ) @@ -38,7 +37,7 @@ func (r DryRunApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx contex return nil } -func (r DryRunApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) error { +func (r DryRunApiserverRepository) StoreSLOs(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) error { r.logger.Infof("Dry run StoreSLOs") return nil } diff --git a/internal/storage/k8s/fake.go b/internal/storage/k8s/fake.go index 9f7c0822..152b5283 100644 --- a/internal/storage/k8s/fake.go +++ b/internal/storage/k8s/fake.go @@ -9,7 +9,6 @@ import ( "k8s.io/apimachinery/pkg/watch" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/storage" "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientsetfake "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/fake" @@ -42,7 +41,7 @@ func (r FakeApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context. return r.ksvc.EnsurePrometheusServiceLevelStatus(ctx, slo, err) } -func (r FakeApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) error { +func (r FakeApiserverRepository) StoreSLOs(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) error { return r.ksvc.StoreSLOs(ctx, kmeta, slos) } diff --git a/internal/storage/k8s/k8s.go b/internal/storage/k8s/k8s.go index 8656ae5a..00c29d26 100644 --- a/internal/storage/k8s/k8s.go +++ b/internal/storage/k8s/k8s.go @@ -9,12 +9,10 @@ import ( monitoringclientset "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" kubeerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" kubernetesmodelmap "github.com/slok/sloth/internal/kubernetes/modelmap" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/storage" "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientset "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" @@ -65,7 +63,7 @@ func (r ApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Cont return err } -func (r ApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMeta, slos model.PromSLOGroupResult) error { +func (r ApiserverRepository) StoreSLOs(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) error { // Map to the Prometheus operator CRD. rule, err := kubernetesmodelmap.MapModelToPrometheusOperator(ctx, kmeta, slos) if err != nil { @@ -74,10 +72,10 @@ func (r ApiserverRepository) StoreSLOs(ctx context.Context, kmeta storage.K8sMet // Add object reference. rule.ObjectMeta.OwnerReferences = append(rule.ObjectMeta.OwnerReferences, metav1.OwnerReference{ - Kind: kmeta.Kind, - APIVersion: kmeta.APIVersion, - Name: kmeta.Name, - UID: types.UID(kmeta.UID), + Kind: "PrometheusServiceLevel", + APIVersion: "sloth.slok.dev/v1", + Name: slos.OriginalSource.K8sSlothV1.Name, + UID: slos.OriginalSource.K8sSlothV1.UID, }) // Create on API server. diff --git a/internal/storage/k8s/k8s_test.go b/internal/storage/k8s/k8s_test.go index 23e97bcf..7a30b435 100644 --- a/internal/storage/k8s/k8s_test.go +++ b/internal/storage/k8s/k8s_test.go @@ -15,27 +15,27 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/internal/storage" storagek8s "github.com/slok/sloth/internal/storage/k8s" "github.com/slok/sloth/pkg/common/model" + slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientsetfake "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/fake" ) func TestApiserverRepositoryStoreSLOs(t *testing.T) { tests := map[string]struct { - k8sMeta storage.K8sMeta + k8sMeta model.K8sMeta slos model.PromSLOGroupResult expPromOperatorRules []monitoringv1.PrometheusRule expErr bool }{ "Having 0 SLO rules should fail.": { - k8sMeta: storage.K8sMeta{}, + k8sMeta: model.K8sMeta{}, slos: model.PromSLOGroupResult{}, expErr: true, }, "Having 0 SLO rules generated should fail.": { - k8sMeta: storage.K8sMeta{}, + k8sMeta: model.K8sMeta{}, slos: model.PromSLOGroupResult{ SLOResults: []model.PromSLOResult{}, }, @@ -43,131 +43,137 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { }, "Having a mixed example of multiple SLOs and options should ensure them on k8s correctly.": { - k8sMeta: storage.K8sMeta{ + k8sMeta: model.K8sMeta{ Name: "test-name", Namespace: "test-ns", Labels: map[string]string{"lk1": "lv1"}, Annotations: map[string]string{"ak1": "av1"}, - Kind: "test-kind", - APIVersion: "test-apiversion", - UID: "test-uid", }, - slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ - { - SLO: model.PromSLO{ID: "testa"}, - PrometheusRules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{ - Name: "sloth-slo-sli-recordings-testa", - Rules: []rulefmt.Rule{ - { - Record: "test:record-a1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - }, - { - Record: "test:record-a2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - }, - }}, - MetadataRecRules: model.PromRuleGroup{ - Name: "sloth-slo-meta-recordings-testa", - Rules: []rulefmt.Rule{ - { - Record: "test:record-a3", - Expr: "test-expr-a3", - Labels: map[string]string{"test-label": "a-3"}, - }, - { - Record: "test:record-a4", - Expr: "test-expr-a4", - Labels: map[string]string{"test-label": "a-4"}, - }, - }}, - AlertRules: model.PromRuleGroup{ - Name: "sloth-slo-alerts-testa", - Interval: 15 * time.Minute, // Custom interval. - Rules: []rulefmt.Rule{ - { - Alert: "testAlertA1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - Annotations: map[string]string{"test-annot": "a-1"}, - }, - { - Alert: "testAlertA2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - Annotations: map[string]string{"test-annot": "a-2"}, - }, - }}, + slos: model.PromSLOGroupResult{ + OriginalSource: model.PromSLOGroupSource{ + K8sSlothV1: &slothv1.PrometheusServiceLevel{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + UID: types.UID("test-uid"), + }, }, }, - { - SLO: model.PromSLO{ID: "testb"}, - PrometheusRules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{ - Name: "sloth-slo-sli-recordings-testb", - Rules: []rulefmt.Rule{ - { - Record: "test:record-b1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - }, - }}, - MetadataRecRules: model.PromRuleGroup{ - Name: "sloth-slo-meta-recordings-testb", - Rules: []rulefmt.Rule{ - { - Record: "test:record-b2", - Expr: "test-expr-b2", - Labels: map[string]string{"test-label": "b-2"}, - }, - }}, - AlertRules: model.PromRuleGroup{ - Name: "sloth-slo-alerts-testb", - Rules: []rulefmt.Rule{ - { - Alert: "testAlertB1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - Annotations: map[string]string{"test-annot": "b-1"}, - }, - }}, - ExtraRules: []model.PromRuleGroup{ - { - Name: "sloth-slo-extra-rules-000-testb", - Interval: 42 * time.Minute, + SLOResults: []model.PromSLOResult{ + { + SLO: model.PromSLO{ID: "testa"}, + PrometheusRules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-testa", Rules: []rulefmt.Rule{ { - Alert: "testAlertZ1", - Expr: "test-expr-z1", - Labels: map[string]string{"test-label": "z-1"}, - Annotations: map[string]string{"test-annot": "z-1"}, + Record: "test:record-a1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + }, + { + Record: "test:record-a2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, }, }}, - {}, // Should be skipped. - { - Name: "sloth-slo-extra-rules-001-testb", + MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-testa", Rules: []rulefmt.Rule{ { - Alert: "testAlertZ2", - Expr: "test-expr-z2", - Labels: map[string]string{"test-label": "z-2"}, - Annotations: map[string]string{"test-annot": "z-2"}, + Record: "test:record-a3", + Expr: "test-expr-a3", + Labels: map[string]string{"test-label": "a-3"}, }, { - Alert: "testAlertZ3", - Expr: "test-expr-z3", - Labels: map[string]string{"test-label": "z-3"}, - Annotations: map[string]string{"test-annot": "z-3"}, + Record: "test:record-a4", + Expr: "test-expr-a4", + Labels: map[string]string{"test-label": "a-4"}, + }, + }}, + AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-testa", + Interval: 15 * time.Minute, // Custom interval. + Rules: []rulefmt.Rule{ + { + Alert: "testAlertA1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + Annotations: map[string]string{"test-annot": "a-1"}, + }, + { + Alert: "testAlertA2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + Annotations: map[string]string{"test-annot": "a-2"}, + }, + }}, + }, + }, + { + SLO: model.PromSLO{ID: "testb"}, + PrometheusRules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-testb", + Rules: []rulefmt.Rule{ + { + Record: "test:record-b1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + }, + }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-testb", + Rules: []rulefmt.Rule{ + { + Record: "test:record-b2", + Expr: "test-expr-b2", + Labels: map[string]string{"test-label": "b-2"}, + }, + }}, + AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-testb", + Rules: []rulefmt.Rule{ + { + Alert: "testAlertB1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + Annotations: map[string]string{"test-annot": "b-1"}, + }, + }}, + ExtraRules: []model.PromRuleGroup{ + { + Name: "sloth-slo-extra-rules-000-testb", + Interval: 42 * time.Minute, + Rules: []rulefmt.Rule{ + { + Alert: "testAlertZ1", + Expr: "test-expr-z1", + Labels: map[string]string{"test-label": "z-1"}, + Annotations: map[string]string{"test-annot": "z-1"}, + }, + }}, + {}, // Should be skipped. + { + Name: "sloth-slo-extra-rules-001-testb", + Rules: []rulefmt.Rule{ + { + Alert: "testAlertZ2", + Expr: "test-expr-z2", + Labels: map[string]string{"test-label": "z-2"}, + Annotations: map[string]string{"test-annot": "z-2"}, + }, + { + Alert: "testAlertZ3", + Expr: "test-expr-z3", + Labels: map[string]string{"test-label": "z-3"}, + Annotations: map[string]string{"test-annot": "z-3"}, + }, }, }, }, }, }, - }, - }}, + }}, expPromOperatorRules: []monitoringv1.PrometheusRule{ { TypeMeta: metav1.TypeMeta{ @@ -185,8 +191,8 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { Annotations: map[string]string{"ak1": "av1"}, OwnerReferences: []metav1.OwnerReference{ { - Kind: "test-kind", - APIVersion: "test-apiversion", + Kind: "PrometheusServiceLevel", + APIVersion: "sloth.slok.dev/v1", Name: "test-name", UID: types.UID("test-uid"), }, diff --git a/internal/storage/storage.go b/internal/storage/storage.go deleted file mode 100644 index 529bfec6..00000000 --- a/internal/storage/storage.go +++ /dev/null @@ -1,12 +0,0 @@ -package storage - -// K8sMeta is the Kubernetes metadata simplified used for storage purposes. -type K8sMeta struct { - Kind string `validate:"required"` - APIVersion string `validate:"required"` - Name string `validate:"required"` - UID string - Namespace string - Annotations map[string]string - Labels map[string]string -} diff --git a/pkg/common/model/k8s.go b/pkg/common/model/k8s.go new file mode 100644 index 00000000..22aecad0 --- /dev/null +++ b/pkg/common/model/k8s.go @@ -0,0 +1,9 @@ +package model + +// K8sMeta is the Kubernetes simplified metadata used on different parts of Sloth logic like K8s storage. +type K8sMeta struct { + Name string + Namespace string + Annotations map[string]string + Labels map[string]string +} diff --git a/pkg/lib/lib_as_cli_use_cases_test.go b/pkg/lib/lib_as_cli_use_cases_test.go index a040317e..8565cccc 100644 --- a/pkg/lib/lib_as_cli_use_cases_test.go +++ b/pkg/lib/lib_as_cli_use_cases_test.go @@ -59,7 +59,7 @@ func TestLibAsCLIIntegration(t *testing.T) { expOutFilePath: "../../test/integration/prometheus/testdata/out-base-k8s.yaml.tpl", resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { var b bytes.Buffer - kmeta := lib.K8sMeta{Name: "svc", Namespace: "test-ns"} + kmeta := model.K8sMeta{Name: "svc", Namespace: "test-ns"} err := lib.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) require.NoError(t, err) return b.Bytes() @@ -216,7 +216,7 @@ func TestLibAsCLIIntegration(t *testing.T) { expOutFilePath: "../../test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl", resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { var b bytes.Buffer - kmeta := lib.K8sMeta{Name: "svc", Namespace: "test-ns"} + kmeta := model.K8sMeta{Name: "svc", Namespace: "test-ns"} err := lib.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) require.NoError(t, err) return b.Bytes() diff --git a/pkg/lib/lib_test.go b/pkg/lib/lib_test.go index 5fe8414e..2b58a282 100644 --- a/pkg/lib/lib_test.go +++ b/pkg/lib/lib_test.go @@ -4,6 +4,7 @@ import ( "context" "os" + "github.com/slok/sloth/pkg/common/model" slotk8sv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" "github.com/slok/sloth/pkg/lib" slothprometheusv1 "github.com/slok/sloth/pkg/prometheus/api/v1" @@ -184,7 +185,7 @@ func ExamplePrometheusSLOGenerator_GenerateFromK8sV1() { panic(err) } - kmeta := lib.K8sMeta{ + kmeta := model.K8sMeta{ Name: "sloth-slo-gen-" + sloSpec.ObjectMeta.Name, Namespace: sloSpec.ObjectMeta.Namespace, } diff --git a/pkg/lib/storage.go b/pkg/lib/storage.go index 1bae6f0c..0d7bd558 100644 --- a/pkg/lib/storage.go +++ b/pkg/lib/storage.go @@ -4,7 +4,6 @@ import ( "context" "io" - "github.com/slok/sloth/internal/storage" storageio "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" "github.com/slok/sloth/pkg/lib/log" @@ -19,25 +18,9 @@ func WriteResultAsPrometheusStd(ctx context.Context, slo model.PromSLOGroupResul return repo.StoreSLOs(ctx, slo) } -// K8sMeta is the Kubernetes metadata to use when writing Kubernetes related rules. -type K8sMeta struct { - Name string - Namespace string - Annotations map[string]string - Labels map[string]string -} - // WriteResultAsK8sPrometheusOperator writes the SLO results into the writer as a Prometheus Operator CRD file. // More information in: https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.PrometheusRule. -func WriteResultAsK8sPrometheusOperator(ctx context.Context, k8sMeta K8sMeta, slo model.PromSLOGroupResult, w io.Writer) error { +func WriteResultAsK8sPrometheusOperator(ctx context.Context, k8sMeta model.K8sMeta, slo model.PromSLOGroupResult, w io.Writer) error { repo := storageio.NewIOWriterPrometheusOperatorYAMLRepo(w, log.Noop) - - kmeta := storage.K8sMeta{ - Name: k8sMeta.Name, - Namespace: k8sMeta.Namespace, - Annotations: k8sMeta.Annotations, - Labels: k8sMeta.Labels, - } - - return repo.StoreSLOs(ctx, kmeta, slo) + return repo.StoreSLOs(ctx, k8sMeta, slo) } From 713fe1b3461a2327c7041f74f90ffc5e0aa737c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 07:20:22 +0000 Subject: [PATCH 105/173] build(deps): bump actions/upload-artifact from 4 to 5 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/generate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/generate.yaml b/.github/workflows/generate.yaml index 6644187c..b44ff844 100644 --- a/.github/workflows/generate.yaml +++ b/.github/workflows/generate.yaml @@ -19,7 +19,7 @@ jobs: ./sloth-linux-amd64 generate -i ./examples/getting-started.yml -o ./examples/_gen/getting-started.yml ./sloth-linux-amd64 generate -i ./examples/no-alerts.yml -o ./examples/_gen/no-alerts.yml - name: "Upload directory with generated SLOs" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: SLOs path: examples/_gen/ From fee4a7003e56d59365e72f942f67813703b06372 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Mon, 27 Oct 2025 18:36:18 +0100 Subject: [PATCH 106/173] Set Sloth lib storage options into the generator struct Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 64 ++++++++++----------------- cmd/sloth/commands/validate.go | 4 +- pkg/lib/bench_test.go | 2 +- pkg/lib/gen.go | 34 +++++++++++---- pkg/lib/lib_as_cli_use_cases_test.go | 65 +++++++++++++++------------- pkg/lib/lib_test.go | 6 +-- pkg/lib/pkg_example_test.go | 2 +- pkg/lib/storage.go | 26 ----------- 8 files changed, 90 insertions(+), 113 deletions(-) delete mode 100644 pkg/lib/storage.go diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index 19a077fb..c0f4bc77 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -17,7 +17,6 @@ import ( "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/plugin" - storageio "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" slothlib "github.com/slok/sloth/pkg/lib" @@ -237,10 +236,27 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { } for _, genTarget := range genTargets { - err := generateSLOs(ctx, logger, *genService, genTarget, g.disableAlerts, g.disableRecordings) + // Generate SLOs. + genResult, err := genService.GenerateFromRaw(ctx, []byte(genTarget.SLOData)) if err != nil { return fmt.Errorf("could not generate SLOs: %w", err) } + + // Disable data if required. + for i := range genResult.SLOResults { + if g.disableAlerts { + genResult.SLOResults[i].PrometheusRules.AlertRules = model.PromRuleGroup{} + } + if g.disableRecordings { + genResult.SLOResults[i].PrometheusRules.SLIErrorRecRules = model.PromRuleGroup{} + genResult.SLOResults[i].PrometheusRules.MetadataRecRules = model.PromRuleGroup{} + } + } + + err = g.storeSLOs(ctx, logger, genService, *genResult, genTarget.Out) + if err != nil { + return fmt.Errorf("could not store SLOs: %w", err) + } } return nil @@ -251,42 +267,15 @@ type generateTarget struct { SLOData string } -func generateSLOs(ctx context.Context, logger log.Logger, genService slothlib.PrometheusSLOGenerator, genTarget generateTarget, disableAlerts, disableRecordings bool) error { - dataB := []byte(genTarget.SLOData) - - // Generate SLOs. - genResult, err := genService.GenerateFromRaw(ctx, dataB) - if err != nil { - return fmt.Errorf("could not generate SLOs: %w", err) - } - - // Disable data if required. - for i := range genResult.SLOResults { - if disableAlerts { - genResult.SLOResults[i].PrometheusRules.AlertRules = model.PromRuleGroup{} - } - if disableRecordings { - genResult.SLOResults[i].PrometheusRules.SLIErrorRecRules = model.PromRuleGroup{} - genResult.SLOResults[i].PrometheusRules.MetadataRecRules = model.PromRuleGroup{} - } - } - +func (g generateCommand) storeSLOs(ctx context.Context, logger log.Logger, generator *slothlib.PrometheusSLOGenerator, genResult model.PromSLOGroupResult, out io.Writer) error { // Store results. switch { // Standard prometheus. case genResult.OriginalSource.SlothV1 != nil: - repo := storageio.NewStdPrometheusGroupedRulesYAMLRepo(genTarget.Out, logger) - err = repo.StoreSLOs(ctx, *genResult) - if err != nil { - return fmt.Errorf("could not store SLOS: %w", err) - } - - return nil + return generator.WriteResultAsPrometheusStd(ctx, genResult, out) // K8s Sloth CR. case genResult.OriginalSource.K8sSlothV1 != nil: - repo := storageio.NewIOWriterPrometheusOperatorYAMLRepo(genTarget.Out, logger) - kmeta := model.K8sMeta{ Name: genResult.OriginalSource.K8sSlothV1.Name, Namespace: genResult.OriginalSource.K8sSlothV1.Namespace, @@ -294,22 +283,13 @@ func generateSLOs(ctx context.Context, logger log.Logger, genService slothlib.Pr Annotations: genResult.OriginalSource.K8sSlothV1.Annotations, } - err = repo.StoreSLOs(ctx, kmeta, *genResult) - if err != nil { - return fmt.Errorf("could not store SLOS: %w", err) - } + return generator.WriteResultAsK8sPrometheusOperator(ctx, kmeta, genResult, out) // OpenSLO. case genResult.OriginalSource.OpenSLOV1Alpha != nil: - repo := storageio.NewStdPrometheusGroupedRulesYAMLRepo(genTarget.Out, logger) - err = repo.StoreSLOs(ctx, *genResult) - if err != nil { - return fmt.Errorf("could not store SLOS: %w", err) - } + return generator.WriteResultAsPrometheusStd(ctx, genResult, out) default: return fmt.Errorf("invalid spec, could not load with any of the supported spec types") } - - return nil } diff --git a/cmd/sloth/commands/validate.go b/cmd/sloth/commands/validate.go index 51317947..69862b18 100644 --- a/cmd/sloth/commands/validate.go +++ b/cmd/sloth/commands/validate.go @@ -138,7 +138,9 @@ func (v validateCommand) Run(ctx context.Context, config RootConfig) error { SLOData: data, Out: io.Discard, } - err := generateSLOs(ctx, logger, *genService, genTarget, false, false) + + // Generate SLOs. + _, err := genService.GenerateFromRaw(ctx, []byte(genTarget.SLOData)) if err != nil { validation.Errs = append(validation.Errs, fmt.Errorf("invalid SLO: %w", err)) } diff --git a/pkg/lib/bench_test.go b/pkg/lib/bench_test.go index 52c99628..ea7bba99 100644 --- a/pkg/lib/bench_test.go +++ b/pkg/lib/bench_test.go @@ -60,7 +60,7 @@ slos: b.Fatal(err) } - err = lib.WriteResultAsPrometheusStd(ctx, *slo, io.Discard) + err = gen.WriteResultAsPrometheusStd(ctx, *slo, io.Discard) if err != nil { b.Fatal(err) } diff --git a/pkg/lib/gen.go b/pkg/lib/gen.go index 90d74330..1d96b614 100644 --- a/pkg/lib/gen.go +++ b/pkg/lib/gen.go @@ -3,6 +3,7 @@ package lib import ( "context" "fmt" + "io" "io/fs" "time" @@ -11,6 +12,7 @@ import ( "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/info" + storagefs "github.com/slok/sloth/internal/storage/fs" storageio "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" @@ -78,16 +80,14 @@ func (c *PrometheusSLOGeneratorConfig) defaults() error { // PrometheusSLOGenerator is a Prometheus SLO rules generator from the Sloth supported SLO definitions. type PrometheusSLOGenerator struct { - // Generator. - genSvc generate.Service - - // Spec loaders. + genSvc generate.Service promYAMLLoader storageio.SlothPrometheusYAMLSpecLoader kubeYAMLLoader storageio.K8sSlothPrometheusYAMLSpecLoader openSLOYAMLLoader storageio.OpenSLOYAMLSpecLoader - - extraLabels map[string]string - agent CallerAgent + pluginsRepo *storagefs.FilePluginRepo + extraLabels map[string]string + agent CallerAgent + logger log.Logger } func NewPrometheusSLOGenerator(config PrometheusSLOGeneratorConfig) (*PrometheusSLOGenerator, error) { @@ -132,7 +132,7 @@ func NewPrometheusSLOGenerator(config PrometheusSLOGeneratorConfig) (*Prometheus DefaultPlugins: defSLOPlugins, SLOPluginGetter: pluginRepo, ExtraPlugins: config.CMDSLOPlugins, - Logger: log.Noop, + Logger: config.Logger, }) if err != nil { return nil, fmt.Errorf("could not create application service: %w", err) @@ -143,8 +143,10 @@ func NewPrometheusSLOGenerator(config PrometheusSLOGeneratorConfig) (*Prometheus promYAMLLoader: storageio.NewSlothPrometheusYAMLSpecLoader(pluginRepo, config.DefaultSLOPeriod), kubeYAMLLoader: storageio.NewK8sSlothPrometheusYAMLSpecLoader(pluginRepo, config.DefaultSLOPeriod), openSLOYAMLLoader: storageio.NewOpenSLOYAMLSpecLoader(config.DefaultSLOPeriod), + pluginsRepo: pluginRepo, extraLabels: config.ExtraLabels, agent: config.CallerAgent, + logger: config.Logger, }, nil } @@ -297,3 +299,19 @@ func (p PrometheusSLOGenerator) generateFromModel(ctx context.Context, req gener SLOResults: result, }, nil } + +// WriteResultAsPrometheusStd writes the SLO results into the writer as a Prometheus standard rules file. +// More information in: +// - https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/#recording-rules. +// - https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/. +func (p PrometheusSLOGenerator) WriteResultAsPrometheusStd(ctx context.Context, slo model.PromSLOGroupResult, w io.Writer) error { + repo := storageio.NewStdPrometheusGroupedRulesYAMLRepo(w, p.logger) + return repo.StoreSLOs(ctx, slo) +} + +// WriteResultAsK8sPrometheusOperator writes the SLO results into the writer as a Prometheus Operator CRD file. +// More information in: https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.PrometheusRule. +func (p PrometheusSLOGenerator) WriteResultAsK8sPrometheusOperator(ctx context.Context, k8sMeta model.K8sMeta, slo model.PromSLOGroupResult, w io.Writer) error { + repo := storageio.NewIOWriterPrometheusOperatorYAMLRepo(w, p.logger) + return repo.StoreSLOs(ctx, k8sMeta, slo) +} diff --git a/pkg/lib/lib_as_cli_use_cases_test.go b/pkg/lib/lib_as_cli_use_cases_test.go index 8565cccc..bdb5dce1 100644 --- a/pkg/lib/lib_as_cli_use_cases_test.go +++ b/pkg/lib/lib_as_cli_use_cases_test.go @@ -25,7 +25,7 @@ func TestLibAsCLIIntegration(t *testing.T) { tests := map[string]struct { config func() lib.PrometheusSLOGeneratorConfig inFilePath string - resultFormatter func(t *testing.T, result model.PromSLOGroupResult) []byte + resultFormatter func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte expOutFilePath string expGenErr bool }{ @@ -43,9 +43,9 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base.yaml.tpl", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + err := gen.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) return b.Bytes() }, @@ -57,10 +57,10 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base-k8s.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-k8s.yaml.tpl", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { var b bytes.Buffer kmeta := model.K8sMeta{Name: "svc", Namespace: "test-ns"} - err := lib.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) + err := gen.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) require.NoError(t, err) return b.Bytes() }, @@ -72,9 +72,9 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-openslo.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-openslo.yaml.tpl", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + err := gen.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) return b.Bytes() }, @@ -89,9 +89,9 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-28d.yaml.tpl", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + err := gen.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) return b.Bytes() }, @@ -107,9 +107,9 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-custom-windows-7d.yaml.tpl", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + err := gen.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) return b.Bytes() }, @@ -124,9 +124,9 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-extra-labels.yaml.tpl", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + err := gen.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) return b.Bytes() }, @@ -138,14 +138,14 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-no-alerts.yaml.tpl", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { for i := range result.SLOResults { result.SLOResults[i].PrometheusRules.AlertRules = model.PromRuleGroup{} } var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + err := gen.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) return b.Bytes() }, @@ -157,7 +157,7 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-base.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-base-no-recordings.yaml.tpl", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { // Remove alerts. for i := range result.SLOResults { result.SLOResults[i].PrometheusRules.SLIErrorRecRules = model.PromRuleGroup{} @@ -165,7 +165,7 @@ func TestLibAsCLIIntegration(t *testing.T) { } var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + err := gen.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) return b.Bytes() }, @@ -180,9 +180,9 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-sli-plugin.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-sli-plugin.yaml.tpl", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + err := gen.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) return b.Bytes() }, @@ -197,9 +197,9 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-slo-plugin.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-slo-plugin.yaml.tpl", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { var b bytes.Buffer - err := lib.WriteResultAsPrometheusStd(t.Context(), result, &b) + err := gen.WriteResultAsPrometheusStd(t.Context(), result, &b) require.NoError(t, err) return b.Bytes() }, @@ -214,10 +214,10 @@ func TestLibAsCLIIntegration(t *testing.T) { }, inFilePath: "../../test/integration/prometheus/testdata/in-slo-plugin-k8s.yaml", expOutFilePath: "../../test/integration/prometheus/testdata/out-slo-plugin-k8s.yaml.tpl", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { var b bytes.Buffer kmeta := model.K8sMeta{Name: "svc", Namespace: "test-ns"} - err := lib.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) + err := gen.WriteResultAsK8sPrometheusOperator(t.Context(), kmeta, result, &b) require.NoError(t, err) return b.Bytes() }, @@ -227,18 +227,22 @@ func TestLibAsCLIIntegration(t *testing.T) { config: func() lib.PrometheusSLOGeneratorConfig { return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} }, - inFilePath: "../../test/integration/prometheus/testdata/in-multifile.yaml", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { return nil }, - expGenErr: true, + inFilePath: "../../test/integration/prometheus/testdata/in-multifile.yaml", + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { + return nil + }, + expGenErr: true, }, "A multifile Kubernetes case (Not supported).": { config: func() lib.PrometheusSLOGeneratorConfig { return lib.PrometheusSLOGeneratorConfig{CallerAgent: lib.CallerAgentCLI} }, - inFilePath: "../../test/integration/prometheus/testdata/in-multifile-k8s.yaml", - resultFormatter: func(t *testing.T, result model.PromSLOGroupResult) []byte { return nil }, - expGenErr: true, + inFilePath: "../../test/integration/prometheus/testdata/in-multifile-k8s.yaml", + resultFormatter: func(t *testing.T, gen *lib.PrometheusSLOGenerator, result model.PromSLOGroupResult) []byte { + return nil + }, + expGenErr: true, }, } @@ -256,10 +260,9 @@ func TestLibAsCLIIntegration(t *testing.T) { result, err := gen.GenerateFromRaw(t.Context(), expInData) if test.expGenErr { assert.Error(err) - return } else if assert.NoError(err) { // Check result. - resultOutData := test.resultFormatter(t, *result) + resultOutData := test.resultFormatter(t, gen, *result) expOutData := getExpData(t, test.expOutFilePath) assert.Equal(string(expOutData), string(resultOutData)) } diff --git a/pkg/lib/lib_test.go b/pkg/lib/lib_test.go index 2b58a282..61fc603a 100644 --- a/pkg/lib/lib_test.go +++ b/pkg/lib/lib_test.go @@ -63,7 +63,7 @@ slos: panic(err) } - err = lib.WriteResultAsPrometheusStd(ctx, *slo, os.Stdout) + err = gen.WriteResultAsPrometheusStd(ctx, *slo, os.Stdout) if err != nil { panic(err) } @@ -124,7 +124,7 @@ func ExamplePrometheusSLOGenerator_GenerateFromSlothV1() { panic(err) } - err = lib.WriteResultAsPrometheusStd(ctx, *slo, os.Stdout) + err = gen.WriteResultAsPrometheusStd(ctx, *slo, os.Stdout) if err != nil { panic(err) } @@ -189,7 +189,7 @@ func ExamplePrometheusSLOGenerator_GenerateFromK8sV1() { Name: "sloth-slo-gen-" + sloSpec.ObjectMeta.Name, Namespace: sloSpec.ObjectMeta.Namespace, } - err = lib.WriteResultAsK8sPrometheusOperator(ctx, kmeta, *slo, os.Stdout) + err = gen.WriteResultAsK8sPrometheusOperator(ctx, kmeta, *slo, os.Stdout) if err != nil { panic(err) } diff --git a/pkg/lib/pkg_example_test.go b/pkg/lib/pkg_example_test.go index 6e5defbf..8d564582 100644 --- a/pkg/lib/pkg_example_test.go +++ b/pkg/lib/pkg_example_test.go @@ -36,7 +36,7 @@ func Example() { return } w.WriteHeader(http.StatusOK) - err = sloth.WriteResultAsPrometheusStd(r.Context(), *result, w) + err = gen.WriteResultAsPrometheusStd(r.Context(), *result, w) if err != nil { http.Error(w, fmt.Sprintf("could not write result: %v", err), http.StatusInternalServerError) return diff --git a/pkg/lib/storage.go b/pkg/lib/storage.go deleted file mode 100644 index 0d7bd558..00000000 --- a/pkg/lib/storage.go +++ /dev/null @@ -1,26 +0,0 @@ -package lib - -import ( - "context" - "io" - - storageio "github.com/slok/sloth/internal/storage/io" - "github.com/slok/sloth/pkg/common/model" - "github.com/slok/sloth/pkg/lib/log" -) - -// WriteResultAsPrometheusStd writes the SLO results into the writer as a Prometheus standard rules file. -// More information in: -// - https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/#recording-rules. -// - https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/. -func WriteResultAsPrometheusStd(ctx context.Context, slo model.PromSLOGroupResult, w io.Writer) error { - repo := storageio.NewStdPrometheusGroupedRulesYAMLRepo(w, log.Noop) - return repo.StoreSLOs(ctx, slo) -} - -// WriteResultAsK8sPrometheusOperator writes the SLO results into the writer as a Prometheus Operator CRD file. -// More information in: https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.PrometheusRule. -func WriteResultAsK8sPrometheusOperator(ctx context.Context, k8sMeta model.K8sMeta, slo model.PromSLOGroupResult, w io.Writer) error { - repo := storageio.NewIOWriterPrometheusOperatorYAMLRepo(w, log.Noop) - return repo.StoreSLOs(ctx, k8sMeta, slo) -} From d7a8a0416be12f3d13dae18812bd9b1a65096f3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 07:01:59 +0000 Subject: [PATCH 107/173] build(deps): bump github.com/prometheus/common from 0.67.1 to 0.67.2 Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.67.1 to 0.67.2. - [Release notes](https://github.com/prometheus/common/releases) - [Changelog](https://github.com/prometheus/common/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/common/compare/v0.67.1...v0.67.2) --- updated-dependencies: - dependency-name: github.com/prometheus/common dependency-version: 0.67.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 95aeb7cf..faff17c7 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.1 github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.1 github.com/prometheus/client_golang v1.23.2 - github.com/prometheus/common v0.67.1 + github.com/prometheus/common v0.67.2 github.com/prometheus/prometheus v0.307.2 github.com/sirupsen/logrus v1.9.3 github.com/slok/reload v0.2.0 diff --git a/go.sum b/go.sum index 957e7428..05bbc7d4 100644 --- a/go.sum +++ b/go.sum @@ -178,8 +178,8 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= -github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= From cbda01c94f7db14a56828b8ca81347df167badc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 07:01:58 +0000 Subject: [PATCH 108/173] build(deps): bump github.com/prometheus/prometheus Bumps [github.com/prometheus/prometheus](https://github.com/prometheus/prometheus) from 0.307.2 to 0.307.3. - [Release notes](https://github.com/prometheus/prometheus/releases) - [Changelog](https://github.com/prometheus/prometheus/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/prometheus/compare/v0.307.2...v0.307.3) --- updated-dependencies: - dependency-name: github.com/prometheus/prometheus dependency-version: 0.307.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index faff17c7..9c30e6a1 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.1 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/common v0.67.2 - github.com/prometheus/prometheus v0.307.2 + github.com/prometheus/prometheus v0.307.3 github.com/sirupsen/logrus v1.9.3 github.com/slok/reload v0.2.0 github.com/spotahome/kooper/v2 v2.9.0 diff --git a/go.sum b/go.sum index 05bbc7d4..0a3efff2 100644 --- a/go.sum +++ b/go.sum @@ -184,8 +184,8 @@ github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEo github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= -github.com/prometheus/prometheus v0.307.2 h1:oA6J+sgS1iTEpsRjyKUYAe+3BzwpsYBqzeRchPOUZ/M= -github.com/prometheus/prometheus v0.307.2/go.mod h1:UeEsqN3iSmAASRE3qkAm3b/3ofdiTAqGdsz+Sj3F0KA= +github.com/prometheus/prometheus v0.307.3 h1:zGIN3EpiKacbMatcUL2i6wC26eRWXdoXfNPjoBc2l34= +github.com/prometheus/prometheus v0.307.3/go.mod h1:sPbNW+KTS7WmzFIafC3Inzb6oZVaGLnSvwqTdz2jxRQ= github.com/prometheus/sigv4 v0.2.1 h1:hl8D3+QEzU9rRmbKIRwMKRwaFGyLkbPdH5ZerglRHY0= github.com/prometheus/sigv4 v0.2.1/go.mod h1:ySk6TahIlsR2sxADuHy4IBFhwEjRGGsfbbLGhFYFj6Q= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= From 176474f79ed64f90e8376ff13491cb0f48157c57 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Fri, 31 Oct 2025 08:40:16 +0100 Subject: [PATCH 109/173] Prepare for v0.15.0 Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 5 ++++- .../helm/sloth/tests/testdata/output/deployment_default.yaml | 2 +- deploy/kubernetes/helm/sloth/values.yaml | 2 +- deploy/kubernetes/raw/sloth-with-common-plugins.yaml | 2 +- deploy/kubernetes/raw/sloth.yaml | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c26f435a..b4789de1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [v0.15.0] - 2025-10-31 + ### Added - Sloth SLO generation can be used as a Go library in `github.com/slok/sloth/pkg/lib`. @@ -246,7 +248,8 @@ - Support raw query based SLI. - Kubernetes (prometheus-operator) CRD generation support. -[unreleased]: https://github.com/slok/sloth/compare/v0.14.0...HEAD +[unreleased]: https://github.com/slok/sloth/compare/v0.15.0...HEAD +[v0.15.0]: https://github.com/slok/sloth/compare/v0.14.0...v0.15.0 [v0.14.0]: https://github.com/slok/sloth/compare/v0.13.0...v0.14.0 [v0.13.0]: https://github.com/slok/sloth/compare/v0.12.0...v0.13.0 [v0.12.0]: https://github.com/slok/sloth/compare/v0.11.0...v0.12.0 diff --git a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml index 4dfdb814..cf13cd28 100644 --- a/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml +++ b/deploy/kubernetes/helm/sloth/tests/testdata/output/deployment_default.yaml @@ -32,7 +32,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.14.0 + image: ghcr.io/slok/sloth:v0.15.0 args: - kubernetes-controller - --plugins-path=/plugins diff --git a/deploy/kubernetes/helm/sloth/values.yaml b/deploy/kubernetes/helm/sloth/values.yaml index 08ff3794..7c138fc5 100644 --- a/deploy/kubernetes/helm/sloth/values.yaml +++ b/deploy/kubernetes/helm/sloth/values.yaml @@ -6,7 +6,7 @@ labels: {} image: registry: ghcr.io # This field cannot be empty if global.imageRegistry is also empty. repository: slok/sloth - tag: v0.14.0 + tag: v0.15.0 # -- Container resources: requests and limits for CPU, Memory resources: diff --git a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml index 61886c07..81282520 100644 --- a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml +++ b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml @@ -85,7 +85,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.14.0 + image: ghcr.io/slok/sloth:v0.15.0 args: - kubernetes-controller - --plugins-path=/plugins diff --git a/deploy/kubernetes/raw/sloth.yaml b/deploy/kubernetes/raw/sloth.yaml index 09e7a1a8..3a9656e0 100644 --- a/deploy/kubernetes/raw/sloth.yaml +++ b/deploy/kubernetes/raw/sloth.yaml @@ -85,7 +85,7 @@ spec: serviceAccountName: sloth containers: - name: sloth - image: ghcr.io/slok/sloth:v0.14.0 + image: ghcr.io/slok/sloth:v0.15.0 args: - kubernetes-controller - --logger=default From 3b0a2ff69a48285e87b7aea9a49f5045ac4f9a5e Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Fri, 31 Oct 2025 17:23:14 +0100 Subject: [PATCH 110/173] Prepare helm v0.15.0 Signed-off-by: Xabier Larrakoetxea --- deploy/kubernetes/helm/sloth/Chart.yaml | 2 +- deploy/kubernetes/raw/sloth-with-common-plugins.yaml | 12 ++++++------ deploy/kubernetes/raw/sloth.yaml | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/deploy/kubernetes/helm/sloth/Chart.yaml b/deploy/kubernetes/helm/sloth/Chart.yaml index bd829209..7388c99e 100644 --- a/deploy/kubernetes/helm/sloth/Chart.yaml +++ b/deploy/kubernetes/helm/sloth/Chart.yaml @@ -4,4 +4,4 @@ description: Base chart for Sloth. type: application home: https://github.com/slok/sloth kubeVersion: ">= 1.19.0-0" -version: 0.14.0 +version: 0.15.0 diff --git a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml index 81282520..debc3059 100644 --- a/deploy/kubernetes/raw/sloth-with-common-plugins.yaml +++ b/deploy/kubernetes/raw/sloth-with-common-plugins.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -133,7 +133,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth diff --git a/deploy/kubernetes/raw/sloth.yaml b/deploy/kubernetes/raw/sloth.yaml index 3a9656e0..3ad20fc3 100644 --- a/deploy/kubernetes/raw/sloth.yaml +++ b/deploy/kubernetes/raw/sloth.yaml @@ -6,7 +6,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -18,7 +18,7 @@ kind: ClusterRole metadata: name: sloth labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -38,7 +38,7 @@ kind: ClusterRoleBinding metadata: name: sloth labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -59,7 +59,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -74,7 +74,7 @@ spec: template: metadata: labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth @@ -108,7 +108,7 @@ metadata: name: sloth namespace: monitoring labels: - helm.sh/chart: sloth-0.14.0 + helm.sh/chart: sloth-0.15.0 app.kubernetes.io/managed-by: Helm app: sloth app.kubernetes.io/name: sloth From f7c5953cab6c89b02f56844b05cd432d7fcaa852 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sat, 1 Nov 2025 12:42:41 +0100 Subject: [PATCH 111/173] Add k8s-transformer plugins, these allow storing generated SLOs as any type of k8s object using the unstructured system Signed-off-by: Xabier Larrakoetxea --- .mockery.yml | 2 +- CHANGELOG.md | 10 + cmd/sloth/commands/generate.go | 7 +- cmd/sloth/commands/k8scontroller.go | 50 +- internal/kubernetes/modelmap/slo.go | 126 ---- .../plugin.go | 63 ++ .../plugin_test.go | 155 +++++ internal/plugin/plugin.go | 4 + .../k8stransform/custom/custom.go | 21 + .../custom/github_com-caarlos0-env-v11.go | 36 ++ ...b_com-slok-sloth-pkg-common-conventions.go | 42 ++ .../github_com-slok-sloth-pkg-common-model.go | 48 ++ ...ub_com-slok-sloth-pkg-common-utils-data.go | 16 + ...hub_com-slok-sloth-pkg-common-utils-k8s.go | 16 + ...-slok-sloth-pkg-common-utils-prometheus.go | 16 + ...ub_com-slok-sloth-pkg-common-validation.go | 48 ++ ...h-pkg-prometheus-plugin-k8stransform-v1.go | 42 ++ ...machinery-pkg-apis-meta-v1-unstructured.go | 38 ++ .../pluginengine/k8stransform/k8stransform.go | 123 ++++ .../k8stransform/k8stransform_test.go | 250 ++++++++ internal/storage/fs/fsmock/mocks.go | 96 +++ internal/storage/fs/plugin.go | 86 ++- internal/storage/fs/plugin_test.go | 74 ++- internal/storage/io/k8s_obj.go | 62 ++ ...theus_operator_test.go => k8s_obj_test.go} | 188 +----- internal/storage/io/prometheus_operator.go | 52 -- internal/storage/k8s/fake.go | 45 +- internal/storage/k8s/k8s.go | 166 ++++-- internal/storage/k8s/k8s_test.go | 556 +++++++++--------- pkg/common/utils/k8s/k8s.go | 64 ++ pkg/common/utils/k8s/k8s_test.go | 86 +++ pkg/lib/gen.go | 17 +- pkg/lib/helper.go | 5 +- pkg/lib/lib_as_cli_use_cases_test.go | 12 +- .../plugin/k8stransform/testing/testing.go | 68 +++ pkg/prometheus/plugin/k8stransform/v1/v1.go | 37 ++ 36 files changed, 1977 insertions(+), 750 deletions(-) delete mode 100644 internal/kubernetes/modelmap/slo.go create mode 100644 internal/plugin/k8stransform/prom_operator_prometheus_rule_v1/plugin.go create mode 100644 internal/plugin/k8stransform/prom_operator_prometheus_rule_v1/plugin_test.go create mode 100644 internal/pluginengine/k8stransform/custom/custom.go create mode 100644 internal/pluginengine/k8stransform/custom/github_com-caarlos0-env-v11.go create mode 100644 internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-conventions.go create mode 100644 internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-model.go create mode 100644 internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-data.go create mode 100644 internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-k8s.go create mode 100644 internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go create mode 100644 internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-validation.go create mode 100644 internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-prometheus-plugin-k8stransform-v1.go create mode 100644 internal/pluginengine/k8stransform/custom/k8s_io-apimachinery-pkg-apis-meta-v1-unstructured.go create mode 100644 internal/pluginengine/k8stransform/k8stransform.go create mode 100644 internal/pluginengine/k8stransform/k8stransform_test.go create mode 100644 internal/storage/io/k8s_obj.go rename internal/storage/io/{prometheus_operator_test.go => k8s_obj_test.go} (61%) delete mode 100644 internal/storage/io/prometheus_operator.go create mode 100644 pkg/common/utils/k8s/k8s.go create mode 100644 pkg/common/utils/k8s/k8s_test.go create mode 100644 pkg/prometheus/plugin/k8stransform/testing/testing.go create mode 100644 pkg/prometheus/plugin/k8stransform/v1/v1.go diff --git a/.mockery.yml b/.mockery.yml index dafcf79b..7d97c64d 100644 --- a/.mockery.yml +++ b/.mockery.yml @@ -6,4 +6,4 @@ pkgname: '{{.SrcPackageName}}mock' template: testify packages: github.com/slok/sloth/internal/app/generate: {interfaces: {SLOPluginGetter}} - github.com/slok/sloth/internal/storage/fs: {interfaces: {SLIPluginLoader,SLOPluginLoader}} + github.com/slok/sloth/internal/storage/fs: {interfaces: {SLIPluginLoader,SLOPluginLoader, K8sTransformPluginLoader}} diff --git a/CHANGELOG.md b/CHANGELOG.md index b4789de1..cc95cd30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] +### Added + +- K8s transformer plugins to be able to customize the k8s resulting objects without depending on current Prometheys operator Rule CR. +- `sloth.dev/k8stransform/prom-operator-prometheus-rule/v1` K8s transformer plugin. +- Users can now create multiple K8s objects as the output of the SLO generated rules. + +### Changed + +- By default sloth now uses a dynamic `unstructured` plugin (`sloth.dev/k8stransform/prom-operator-prometheus-rule/v1`) to create and manager the prometheus operator Rule K8s CRs. + ## [v0.15.0] - 2025-10-31 ### Added diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index c0f4bc77..b75fbd70 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -17,6 +17,7 @@ import ( "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/plugin" + k8stransformpromopv1 "github.com/slok/sloth/internal/plugin/k8stransform/prom_operator_prometheus_rule_v1" "github.com/slok/sloth/pkg/common/model" utilsdata "github.com/slok/sloth/pkg/common/utils/data" slothlib "github.com/slok/sloth/pkg/lib" @@ -35,6 +36,7 @@ type generateCommand struct { sloPeriod string sloPlugins []string disableDefaultSLOPlugins bool + k8sTransformPluginID string } // NewGenerateCommand returns the generate command. @@ -49,11 +51,12 @@ func NewGenerateCommand(app *kingpin.Application) Command { cmd.Flag("extra-labels", "Extra labels that will be added to all the generated Prometheus rules ('key=value' form, can be repeated).").Short('l').StringMapVar(&c.extraLabels) cmd.Flag("disable-recordings", "Disables recording rules generation.").BoolVar(&c.disableRecordings) cmd.Flag("disable-alerts", "Disables alert rules generation.").BoolVar(&c.disableAlerts) - cmd.Flag("plugins-path", "The path to SLI and SLO plugins (can be repeated).").Short('p').StringsVar(&c.pluginsPaths) + cmd.Flag("plugins-path", "The path to any of the sloth compatible plugin types (can be repeated).").Short('p').StringsVar(&c.pluginsPaths) cmd.Flag("slo-period-windows-path", "The directory path to custom SLO period windows catalog (replaces default ones).").StringVar(&c.sloPeriodWindowsPath) cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) cmd.Flag("slo-plugins", `SLO plugins chain declaration in JSON format '{"id": "foo","priority": 0,"config": "{}"}' (Can be repeated).`).Short('s').StringsVar(&c.sloPlugins) cmd.Flag("disable-default-slo-plugins", `Disables the default SLO plugins, normally used along with custom SLO plugins to fully customize Sloth behavior`).BoolVar(&c.disableDefaultSLOPlugins) + cmd.Flag("k8s-transform-plugin-id", "The ID of the plugin that will transform generated SLOs into k8s objects.").Default(k8stransformpromopv1.PluginID).StringVar(&c.k8sTransformPluginID) return c } @@ -211,7 +214,7 @@ func (g generateCommand) Run(ctx context.Context, config RootConfig) error { } } - pluginsFSs := []fs.FS{plugin.EmbeddedDefaultSLOPlugins} + pluginsFSs := []fs.FS{plugin.EmbeddedDefaultSLOPlugins, plugin.EmbeddedDefaultK8sTransformPlugins} for _, p := range g.pluginsPaths { pluginsFSs = append(pluginsFSs, os.DirFS(p)) } diff --git a/cmd/sloth/commands/k8scontroller.go b/cmd/sloth/commands/k8scontroller.go index 55a141ff..bf83eb2c 100644 --- a/cmd/sloth/commands/k8scontroller.go +++ b/cmd/sloth/commands/k8scontroller.go @@ -14,7 +14,6 @@ import ( "github.com/alecthomas/kingpin/v2" "github.com/oklog/run" - monitoringclientset "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" "github.com/prometheus/client_golang/prometheus/promhttp" prometheusmodel "github.com/prometheus/common/model" "github.com/slok/reload" @@ -24,6 +23,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" _ "k8s.io/client-go/plugin/pkg/client/auth" // Init all available Kube client auth systems. "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -34,6 +35,8 @@ import ( "github.com/slok/sloth/internal/app/kubecontroller" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/internal/plugin" + k8stransformpromopv1 "github.com/slok/sloth/internal/plugin/k8stransform/prom_operator_prometheus_rule_v1" + pluginenginesk8stransform "github.com/slok/sloth/internal/pluginengine/k8stransform" pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" storagefs "github.com/slok/sloth/internal/storage/fs" @@ -74,6 +77,7 @@ type kubeControllerCommand struct { sloPeriod string sloPlugins []string disableDefaultSLOPlugins bool + k8sTransformPluginID string } // NewKubeControllerCommand returns the Kubernetes controller command. @@ -102,6 +106,7 @@ func NewKubeControllerCommand(app *kingpin.Application) Command { cmd.Flag("default-slo-period", "The default SLO period windows to be used for the SLOs.").Default("30d").StringVar(&c.sloPeriod) cmd.Flag("slo-plugins", `SLO plugins chain declaration in JSON format '{"id": "foo","priority": 0,"config": "{}"}' (Can be repeated).`).Short('s').StringsVar(&c.sloPlugins) cmd.Flag("disable-default-slo-plugins", `Disables the default SLO plugins, normally used along with custom SLO plugins to fully customize Sloth behavior`).BoolVar(&c.disableDefaultSLOPlugins) + cmd.Flag("k8s-transform-plugin-id", "The ID of the plugin that will transform generated SLOs into k8s objects.").Default(k8stransformpromopv1.PluginID).StringVar(&c.k8sTransformPluginID) return c } @@ -149,7 +154,7 @@ func (k kubeControllerCommand) Run(ctx context.Context, config RootConfig) error } // Kubernetes services. - kuberepo, err := k.newKubernetesService(ctx, config) + kuberepo, err := k.newKubernetesService(ctx, config, pluginsRepo) if err != nil { return fmt.Errorf("could not create Kubernetes service: %w", err) } @@ -390,12 +395,22 @@ type kubernetesService interface { StoreSLOs(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) error } -func (k kubeControllerCommand) newKubernetesService(ctx context.Context, config RootConfig) (kubernetesService, error) { +func (k kubeControllerCommand) newKubernetesService(ctx context.Context, config RootConfig, pluginsRepo *storagefs.FilePluginRepo) (kubernetesService, error) { config.Logger.Infof("Loading Kubernetes configuration...") + // Get k8s transform plugin. + pluginFact, err := pluginsRepo.GetK8sTransformPlugin(ctx, k.k8sTransformPluginID) + if err != nil { + return nil, fmt.Errorf("could not get k8s transform plugin %q: %w", k.k8sTransformPluginID, err) + } + plugin, err := pluginFact.PluginK8sTransformV1() + if err != nil { + return nil, fmt.Errorf("could not create k8s transform plugin %q: %w", k.k8sTransformPluginID, err) + } + // Fake mode. if k.runMode == controllerModeFake { - return storagek8s.NewFakeApiserverRepository(config.Logger), nil + return storagek8s.NewFakeApiserverRepository(config.Logger, plugin) } // Load Kubernetes clients. @@ -409,18 +424,34 @@ func (k kubeControllerCommand) newKubernetesService(ctx context.Context, config return nil, fmt.Errorf("could not create Kubernetes sloth client: %w", err) } - kubeMonitoringCli, err := monitoringclientset.NewForConfig(kubeCfg) + // Get required clients. + dynamicCli, err := dynamic.NewForConfig(kubeCfg) if err != nil { - return nil, fmt.Errorf("could not create Kubernetes monitoring (prometheus-operator) client: %w", err) + return nil, fmt.Errorf("could not create Kubernetes dynamic client: %w", err) + } + discoveryCli, err := discovery.NewDiscoveryClientForConfig(kubeCfg) + if err != nil { + return nil, fmt.Errorf("could not create Kubernetes discovery client: %w", err) + } + + apiserverRepoConfig := storagek8s.ApiserverRepositoryConfig{ + SlothCli: kubeSlothcli, + Logger: config.Logger, + DynamicCli: dynamicCli, + DiscoveryCli: discoveryCli, + K8sTransformPlugin: plugin, } // Create Kubernetes service. - kuberepo := storagek8s.NewApiserverRepository(kubeSlothcli, kubeMonitoringCli, config.Logger) + kuberepo, err := storagek8s.NewApiserverRepository(apiserverRepoConfig) + if err != nil { + return nil, fmt.Errorf("could not create Kubernetes API server repository: %w", err) + } // Dry run mode. if k.runMode == controllerModeDryRun { config.Logger.Warningf("Kubernetes in dry run mode") - return storagek8s.NewDryRunApiserverRepository(kuberepo, config.Logger), nil + return storagek8s.NewDryRunApiserverRepository(*kuberepo, config.Logger), nil } // Default mode. @@ -491,12 +522,13 @@ func (g generatorLogger) WithCtxValues(ctx context.Context) log.Logger { func createPluginLoader(ctx context.Context, logger log.Logger, paths []string) (*storagefs.FilePluginRepo, error) { fss := []fs.FS{ plugin.EmbeddedDefaultSLOPlugins, + plugin.EmbeddedDefaultK8sTransformPlugins, } for _, p := range paths { fss = append(fss, os.DirFS(p)) } - pluginsRepo, err := storagefs.NewFilePluginRepo(logger, false, pluginenginesli.PluginLoader, pluginengineslo.PluginLoader, fss...) + pluginsRepo, err := storagefs.NewFilePluginRepo(logger, false, pluginenginesli.PluginLoader, pluginengineslo.PluginLoader, pluginenginesk8stransform.PluginLoader, fss...) if err != nil { return nil, fmt.Errorf("could not create file SLO and SLI plugins repository: %w", err) } diff --git a/internal/kubernetes/modelmap/slo.go b/internal/kubernetes/modelmap/slo.go deleted file mode 100644 index c8aa510c..00000000 --- a/internal/kubernetes/modelmap/slo.go +++ /dev/null @@ -1,126 +0,0 @@ -package modelmap - -import ( - "context" - "fmt" - "time" - - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - "github.com/prometheus/prometheus/model/rulefmt" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - - commonerrors "github.com/slok/sloth/pkg/common/errors" - "github.com/slok/sloth/pkg/common/model" - promutils "github.com/slok/sloth/pkg/common/utils/prometheus" -) - -func MapModelToPrometheusOperator(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) (*monitoringv1.PrometheusRule, error) { - // Add extra labels. - labels := map[string]string{ - "app.kubernetes.io/component": "SLO", - "app.kubernetes.io/managed-by": "sloth", - } - for k, v := range kmeta.Labels { - labels[k] = v - } - - rule := &monitoringv1.PrometheusRule{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "monitoring.coreos.com/v1", - Kind: "PrometheusRule", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: kmeta.Name, - Namespace: kmeta.Namespace, - Labels: labels, - Annotations: kmeta.Annotations, - }, - } - - if len(slos.SLOResults) == 0 { - return nil, fmt.Errorf("slo rules required") - } - - for _, slo := range slos.SLOResults { - if len(slo.PrometheusRules.SLIErrorRecRules.Rules) > 0 { - rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Interval: timeDurationToPromOpDuration(slo.PrometheusRules.SLIErrorRecRules.Interval), - Name: slo.PrometheusRules.SLIErrorRecRules.Name, - Rules: promRulesToKubeRules(slo.PrometheusRules.SLIErrorRecRules.Rules), - }) - } - - if len(slo.PrometheusRules.MetadataRecRules.Rules) > 0 { - rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Interval: timeDurationToPromOpDuration(slo.PrometheusRules.MetadataRecRules.Interval), - Name: slo.PrometheusRules.MetadataRecRules.Name, - Rules: promRulesToKubeRules(slo.PrometheusRules.MetadataRecRules.Rules), - }) - } - - if len(slo.PrometheusRules.AlertRules.Rules) > 0 { - rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Interval: timeDurationToPromOpDuration(slo.PrometheusRules.AlertRules.Interval), - Name: slo.PrometheusRules.AlertRules.Name, - Rules: promRulesToKubeRules(slo.PrometheusRules.AlertRules.Rules), - }) - } - - // Extra rules. - for _, extraRuleGroup := range slo.PrometheusRules.ExtraRules { - if len(extraRuleGroup.Rules) == 0 { - continue - } - - rule.Spec.Groups = append(rule.Spec.Groups, monitoringv1.RuleGroup{ - Interval: timeDurationToPromOpDuration(extraRuleGroup.Interval), - Name: extraRuleGroup.Name, - Rules: promRulesToKubeRules(extraRuleGroup.Rules), - }) - } - } - - // If we don't have anything to store, error so we can increase the reliability - // because maybe this was due to an unintended error (typos, misconfig, too many disable...). - if len(rule.Spec.Groups) == 0 { - return nil, commonerrors.ErrNoSLORules - } - - return rule, nil -} - -func promRulesToKubeRules(rules []rulefmt.Rule) []monitoringv1.Rule { - res := make([]monitoringv1.Rule, 0, len(rules)) - for _, r := range rules { - forS := "" - if r.For != 0 { - forS = r.For.String() - } - - var dur *monitoringv1.Duration - if forS != "" { - d := monitoringv1.Duration(forS) - dur = &d - } - - res = append(res, monitoringv1.Rule{ - Record: r.Record, - Alert: r.Alert, - Expr: intstr.FromString(r.Expr), - For: dur, - Labels: r.Labels, - Annotations: r.Annotations, - }) - } - return res -} - -func timeDurationToPromOpDuration(t time.Duration) *monitoringv1.Duration { - if t == 0 { - return nil - } - - r := monitoringv1.Duration(promutils.TimeDurationToPromStr(t)) - return &r -} diff --git a/internal/plugin/k8stransform/prom_operator_prometheus_rule_v1/plugin.go b/internal/plugin/k8stransform/prom_operator_prometheus_rule_v1/plugin.go new file mode 100644 index 00000000..6dcd6043 --- /dev/null +++ b/internal/plugin/k8stransform/prom_operator_prometheus_rule_v1/plugin.go @@ -0,0 +1,63 @@ +package plugin + +import ( + "context" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/slok/sloth/pkg/common/model" + k8sutils "github.com/slok/sloth/pkg/common/utils/k8s" + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" +) + +const ( + PluginVersion = "prometheus/k8stransform/v1" + PluginID = "sloth.dev/k8stransform/prom-operator-prometheus-rule/v1" +) + +func NewPlugin() (plugink8stransformv1.Plugin, error) { + return plugin{}, nil +} + +type plugin struct{} + +func (p plugin) TransformK8sObjects(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*plugink8stransformv1.K8sObjects, error) { + u := &unstructured.Unstructured{} + u.SetAPIVersion("monitoring.coreos.com/v1") + u.SetKind("PrometheusRule") + u.SetNamespace(kmeta.Namespace) + u.SetName(kmeta.Name) + u.SetLabels(kmeta.Labels) + u.SetAnnotations(kmeta.Annotations) + + groups := []any{} + for _, slo := range sloResult.SLOResults { + if len(slo.PrometheusRules.SLIErrorRecRules.Rules) > 0 { + groups = append(groups, k8sutils.PromRuleGroupToUnstructuredPromOperator(slo.PrometheusRules.SLIErrorRecRules)) + } + if len(slo.PrometheusRules.MetadataRecRules.Rules) > 0 { + groups = append(groups, k8sutils.PromRuleGroupToUnstructuredPromOperator(slo.PrometheusRules.MetadataRecRules)) + } + if len(slo.PrometheusRules.AlertRules.Rules) > 0 { + groups = append(groups, k8sutils.PromRuleGroupToUnstructuredPromOperator(slo.PrometheusRules.AlertRules)) + } + + for _, extraRG := range slo.PrometheusRules.ExtraRules { + // Skip empty extra rule groups. + if len(extraRG.Rules) == 0 { + continue + } + groups = append(groups, + k8sutils.PromRuleGroupToUnstructuredPromOperator(extraRG), + ) + } + } + + u.Object["spec"] = map[string]any{ + "groups": groups, + } + + return &plugink8stransformv1.K8sObjects{ + Items: []*unstructured.Unstructured{u}, + }, nil +} diff --git a/internal/plugin/k8stransform/prom_operator_prometheus_rule_v1/plugin_test.go b/internal/plugin/k8stransform/prom_operator_prometheus_rule_v1/plugin_test.go new file mode 100644 index 00000000..03844308 --- /dev/null +++ b/internal/plugin/k8stransform/prom_operator_prometheus_rule_v1/plugin_test.go @@ -0,0 +1,155 @@ +package plugin_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/slok/sloth/pkg/common/model" + plugintesting "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/testing" +) + +func baseSLOGroupResult() model.PromSLOGroupResult { + return model.PromSLOGroupResult{ + SLOResults: []model.PromSLOResult{ + { + SLO: model.PromSLO{ + Name: "test", + }, + PrometheusRules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{ + Name: "slo-test-sli-error-rules", + Interval: 60 * time.Minute, + Rules: []rulefmt.Rule{ + {Record: "rec1", Expr: "exp1"}, + {Record: "rec2", Expr: "exp2", Labels: map[string]string{"team": "team-a"}}, + {Record: "rec3", Expr: "exp3"}, + {Record: "rec4", Expr: "exp4"}, + }, + }, + MetadataRecRules: model.PromRuleGroup{ + Name: "slo-test-metadata-rules", + Interval: 40 * time.Minute, + Rules: []rulefmt.Rule{ + {Record: "rec5", Expr: "exp5", Annotations: map[string]string{"info": "metadata"}}, + {Record: "rec6", Expr: "exp6"}, + {Record: "rec7", Expr: "exp7"}, + }, + }, + AlertRules: model.PromRuleGroup{ + Name: "slo-test-alert-rules", + Rules: []rulefmt.Rule{ + {Alert: "alert1", Expr: "expA", Labels: map[string]string{"severity": "page"}, Annotations: map[string]string{"summary": "High error rate"}}, + {Alert: "alert2", Expr: "expB"}, + }, + }, + ExtraRules: []model.PromRuleGroup{ + { + Name: "slo-test-extra-rules-1", + Interval: 120 * time.Minute, + Rules: []rulefmt.Rule{ + {Record: "rec8", Expr: "exp8"}, + }, + }, + { + Name: "slo-test-extra-rules-2", + Rules: []rulefmt.Rule{ + {Alert: "alert3", Expr: "expC"}, + }, + }, + }, + }, + }, + }, + } +} + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + kmeta model.K8sMeta + slos model.PromSLOGroupResult + expYAML string + }{ + "Test plugin": { + kmeta: model.K8sMeta{ + Namespace: "test-ns", + Name: "test01", + Labels: map[string]string{"app": "sloth"}, + Annotations: map[string]string{"annotation1": "value1"}, + }, + slos: baseSLOGroupResult(), + expYAML: ` +--- +# Code generated by Sloth (dev): https://github.com/slok/sloth. +# DO NOT EDIT. + +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + annotations: + annotation1: value1 + labels: + app: sloth + app.kubernetes.io/component: SLO + app.kubernetes.io/managed-by: sloth + name: test01 + namespace: test-ns +spec: + groups: + - interval: 1h + name: slo-test-sli-error-rules + rules: + - expr: exp1 + record: rec1 + - expr: exp2 + labels: + team: team-a + record: rec2 + - expr: exp3 + record: rec3 + - expr: exp4 + record: rec4 + - interval: 40m + name: slo-test-metadata-rules + rules: + - annotations: + info: metadata + expr: exp5 + record: rec5 + - expr: exp6 + record: rec6 + - expr: exp7 + record: rec7 + - name: slo-test-alert-rules + rules: + - alert: alert1 + annotations: + summary: High error rate + expr: expA + labels: + severity: page + - alert: alert2 + expr: expB + - interval: 2h + name: slo-test-extra-rules-1 + rules: + - expr: exp8 + record: rec8 + - name: slo-test-extra-rules-2 + rules: + - alert: alert3 + expr: expC +`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + pt, err := plugintesting.NewPluginTester("") + require.NoError(t, err) + pt.AssertYAML(t, test.expYAML, test.kmeta, test.slos) + }) + } +} diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 614892a4..7df651b1 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -6,4 +6,8 @@ var ( //go:embed slo // Default SLO plugins. These are the default set of SLO plugins that are embedded in the binary. EmbeddedDefaultSLOPlugins embed.FS + + //go:embed k8stransform + // Default K8s transform plugins. These are the default set of K8s transform plugins that are embedded in the binary. + EmbeddedDefaultK8sTransformPlugins embed.FS ) diff --git a/internal/pluginengine/k8stransform/custom/custom.go b/internal/pluginengine/k8stransform/custom/custom.go new file mode 100644 index 00000000..71a5bddc --- /dev/null +++ b/internal/pluginengine/k8stransform/custom/custom.go @@ -0,0 +1,21 @@ +package custom + +import ( + "reflect" + + _ "github.com/caarlos0/env/v11" // Used only by yaegi plugins, not by Sloth. +) + +//go:generate yaegi extract --name custom github.com/caarlos0/env/v11 + +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1 +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/conventions +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/model +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/utils/data +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/utils/prometheus +//go:generate yaegi extract --name custom github.com/slok/sloth/pkg/common/utils/k8s + +//go:generate yaegi extract --name custom k8s.io/apimachinery/pkg/apis/meta/v1/unstructured + +// Symbols variable stores the map of custom Yaegi symbols per package. +var Symbols = map[string]map[string]reflect.Value{} diff --git a/internal/pluginengine/k8stransform/custom/github_com-caarlos0-env-v11.go b/internal/pluginengine/k8stransform/custom/github_com-caarlos0-env-v11.go new file mode 100644 index 00000000..a8fc3039 --- /dev/null +++ b/internal/pluginengine/k8stransform/custom/github_com-caarlos0-env-v11.go @@ -0,0 +1,36 @@ +// Code generated by 'yaegi extract github.com/caarlos0/env/v11'. DO NOT EDIT. + +package custom + +import ( + "github.com/caarlos0/env/v11" + "reflect" +) + +func init() { + Symbols["github.com/caarlos0/env/v11/env"] = map[string]reflect.Value{ + // function, constant and variable definitions + "GetFieldParams": reflect.ValueOf(env.GetFieldParams), + "GetFieldParamsWithOptions": reflect.ValueOf(env.GetFieldParamsWithOptions), + "Parse": reflect.ValueOf(env.Parse), + "ParseWithOptions": reflect.ValueOf(env.ParseWithOptions), + "ToMap": reflect.ValueOf(env.ToMap), + + // type definitions + "AggregateError": reflect.ValueOf((*env.AggregateError)(nil)), + "EmptyEnvVarError": reflect.ValueOf((*env.EmptyEnvVarError)(nil)), + "EmptyVarError": reflect.ValueOf((*env.EmptyVarError)(nil)), + "EnvVarIsNotSetError": reflect.ValueOf((*env.EnvVarIsNotSetError)(nil)), + "FieldParams": reflect.ValueOf((*env.FieldParams)(nil)), + "LoadFileContentError": reflect.ValueOf((*env.LoadFileContentError)(nil)), + "NoParserError": reflect.ValueOf((*env.NoParserError)(nil)), + "NoSupportedTagOptionError": reflect.ValueOf((*env.NoSupportedTagOptionError)(nil)), + "NotStructPtrError": reflect.ValueOf((*env.NotStructPtrError)(nil)), + "OnSetFn": reflect.ValueOf((*env.OnSetFn)(nil)), + "Options": reflect.ValueOf((*env.Options)(nil)), + "ParseError": reflect.ValueOf((*env.ParseError)(nil)), + "ParseValueError": reflect.ValueOf((*env.ParseValueError)(nil)), + "ParserFunc": reflect.ValueOf((*env.ParserFunc)(nil)), + "VarIsNotSetError": reflect.ValueOf((*env.VarIsNotSetError)(nil)), + } +} diff --git a/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-conventions.go b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-conventions.go new file mode 100644 index 00000000..e1db5112 --- /dev/null +++ b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-conventions.go @@ -0,0 +1,42 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/conventions'. DO NOT EDIT. + +package custom + +import ( + "github.com/slok/sloth/pkg/common/conventions" + "go/constant" + "go/token" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/common/conventions/conventions"] = map[string]reflect.Value{ + // function, constant and variable definitions + "GetSLIErrorMetric": reflect.ValueOf(conventions.GetSLIErrorMetric), + "GetSLOIDPromLabels": reflect.ValueOf(conventions.GetSLOIDPromLabels), + "NameRegexp": reflect.ValueOf(&conventions.NameRegexp).Elem(), + "PromMetaSLOCurrentBurnRateRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:current_burn_rate:ratio\"", token.STRING, 0)), + "PromMetaSLOErrorBudgetRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:error_budget:ratio\"", token.STRING, 0)), + "PromMetaSLOInfoMetric": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_slo_info\"", token.STRING, 0)), + "PromMetaSLOObjectiveRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:objective:ratio\"", token.STRING, 0)), + "PromMetaSLOPeriodBurnRateRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:period_burn_rate:ratio\"", token.STRING, 0)), + "PromMetaSLOPeriodErrorBudgetRemainingRatioMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:period_error_budget_remaining:ratio\"", token.STRING, 0)), + "PromMetaSLOTimePeriodDaysMetric": reflect.ValueOf(constant.MakeFromLiteral("\"slo:time_period:days\"", token.STRING, 0)), + "PromRuleGroupNameSLOAlertsPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"sloth-slo-alerts-\"", token.STRING, 0)), + "PromRuleGroupNameSLOExtraRulesPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"sloth-slo-extra-rules-\"", token.STRING, 0)), + "PromRuleGroupNameSLOMetadataPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"sloth-slo-meta-recordings-\"", token.STRING, 0)), + "PromRuleGroupNameSLOSLIPrefix": reflect.ValueOf(constant.MakeFromLiteral("\"sloth-slo-sli-recordings-\"", token.STRING, 0)), + "PromSLIErrorMetricFmt": reflect.ValueOf(constant.MakeFromLiteral("\"slo:sli_error:ratio_rate%s\"", token.STRING, 0)), + "PromSLOIDLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_id\"", token.STRING, 0)), + "PromSLOModeLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_mode\"", token.STRING, 0)), + "PromSLONameLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_slo\"", token.STRING, 0)), + "PromSLOObjectiveLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_objective\"", token.STRING, 0)), + "PromSLOServiceLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_service\"", token.STRING, 0)), + "PromSLOSeverityLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_severity\"", token.STRING, 0)), + "PromSLOSpecLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_spec\"", token.STRING, 0)), + "PromSLOVersionLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_version\"", token.STRING, 0)), + "PromSLOWindowLabelName": reflect.ValueOf(constant.MakeFromLiteral("\"sloth_window\"", token.STRING, 0)), + "TplSLIQueryWindowVarName": reflect.ValueOf(&conventions.TplSLIQueryWindowVarName).Elem(), + "TplSLIQueryWindowVarRegex": reflect.ValueOf(&conventions.TplSLIQueryWindowVarRegex).Elem(), + } +} diff --git a/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-model.go b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-model.go new file mode 100644 index 00000000..83fabeaa --- /dev/null +++ b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-model.go @@ -0,0 +1,48 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/model'. DO NOT EDIT. + +package custom + +import ( + "github.com/slok/sloth/pkg/common/model" + "go/constant" + "go/token" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/common/model/model"] = map[string]reflect.Value{ + // function, constant and variable definitions + "ModeAPIGenKubernetes": reflect.ValueOf(constant.MakeFromLiteral("\"api-gen-k8s\"", token.STRING, 0)), + "ModeAPIGenOpenSLO": reflect.ValueOf(constant.MakeFromLiteral("\"api-gen-openslo\"", token.STRING, 0)), + "ModeAPIGenPrometheus": reflect.ValueOf(constant.MakeFromLiteral("\"api-gen-prom\"", token.STRING, 0)), + "ModeCLIGenKubernetes": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-k8s\"", token.STRING, 0)), + "ModeCLIGenOpenSLO": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-openslo\"", token.STRING, 0)), + "ModeCLIGenPrometheus": reflect.ValueOf(constant.MakeFromLiteral("\"cli-gen-prom\"", token.STRING, 0)), + "ModeControllerGenKubernetes": reflect.ValueOf(constant.MakeFromLiteral("\"ctrl-gen-k8s\"", token.STRING, 0)), + "ModeTest": reflect.ValueOf(constant.MakeFromLiteral("\"test\"", token.STRING, 0)), + "PageAlertSeverity": reflect.ValueOf(model.PageAlertSeverity), + "TicketAlertSeverity": reflect.ValueOf(model.TicketAlertSeverity), + "UnknownAlertSeverity": reflect.ValueOf(model.UnknownAlertSeverity), + + // type definitions + "AlertSeverity": reflect.ValueOf((*model.AlertSeverity)(nil)), + "Info": reflect.ValueOf((*model.Info)(nil)), + "K8sMeta": reflect.ValueOf((*model.K8sMeta)(nil)), + "MWMBAlert": reflect.ValueOf((*model.MWMBAlert)(nil)), + "MWMBAlertGroup": reflect.ValueOf((*model.MWMBAlertGroup)(nil)), + "Mode": reflect.ValueOf((*model.Mode)(nil)), + "PromAlertMeta": reflect.ValueOf((*model.PromAlertMeta)(nil)), + "PromRuleGroup": reflect.ValueOf((*model.PromRuleGroup)(nil)), + "PromSLI": reflect.ValueOf((*model.PromSLI)(nil)), + "PromSLIEvents": reflect.ValueOf((*model.PromSLIEvents)(nil)), + "PromSLIRaw": reflect.ValueOf((*model.PromSLIRaw)(nil)), + "PromSLO": reflect.ValueOf((*model.PromSLO)(nil)), + "PromSLOGroup": reflect.ValueOf((*model.PromSLOGroup)(nil)), + "PromSLOGroupResult": reflect.ValueOf((*model.PromSLOGroupResult)(nil)), + "PromSLOGroupSource": reflect.ValueOf((*model.PromSLOGroupSource)(nil)), + "PromSLOPluginMetadata": reflect.ValueOf((*model.PromSLOPluginMetadata)(nil)), + "PromSLOResult": reflect.ValueOf((*model.PromSLOResult)(nil)), + "PromSLORules": reflect.ValueOf((*model.PromSLORules)(nil)), + "SLOPlugins": reflect.ValueOf((*model.SLOPlugins)(nil)), + } +} diff --git a/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-data.go b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-data.go new file mode 100644 index 00000000..09751b38 --- /dev/null +++ b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-data.go @@ -0,0 +1,16 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/utils/data'. DO NOT EDIT. + +package custom + +import ( + "github.com/slok/sloth/pkg/common/utils/data" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/common/utils/data/data"] = map[string]reflect.Value{ + // function, constant and variable definitions + "MergeLabels": reflect.ValueOf(data.MergeLabels), + "SplitYAML": reflect.ValueOf(data.SplitYAML), + } +} diff --git a/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-k8s.go b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-k8s.go new file mode 100644 index 00000000..18a920d9 --- /dev/null +++ b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-k8s.go @@ -0,0 +1,16 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/utils/k8s'. DO NOT EDIT. + +package custom + +import ( + "github.com/slok/sloth/pkg/common/utils/k8s" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/common/utils/k8s/k8s"] = map[string]reflect.Value{ + // function, constant and variable definitions + "PromRuleGroupToUnstructuredPromOperator": reflect.ValueOf(k8s.PromRuleGroupToUnstructuredPromOperator), + "UnstructuredToYAMLString": reflect.ValueOf(k8s.UnstructuredToYAMLString), + } +} diff --git a/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go new file mode 100644 index 00000000..d9f81ad9 --- /dev/null +++ b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-utils-prometheus.go @@ -0,0 +1,16 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/utils/prometheus'. DO NOT EDIT. + +package custom + +import ( + "github.com/slok/sloth/pkg/common/utils/prometheus" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/common/utils/prometheus/prometheus"] = map[string]reflect.Value{ + // function, constant and variable definitions + "LabelsToPromFilter": reflect.ValueOf(prometheus.LabelsToPromFilter), + "TimeDurationToPromStr": reflect.ValueOf(prometheus.TimeDurationToPromStr), + } +} diff --git a/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-validation.go b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-validation.go new file mode 100644 index 00000000..2e61afd5 --- /dev/null +++ b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-common-validation.go @@ -0,0 +1,48 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/common/validation'. DO NOT EDIT. + +package custom + +import ( + "github.com/slok/sloth/pkg/common/validation" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/common/validation/validation"] = map[string]reflect.Value{ + // function, constant and variable definitions + "PromQLDialectValidator": reflect.ValueOf(validation.PromQLDialectValidator), + "ValidateSLO": reflect.ValueOf(validation.ValidateSLO), + + // type definitions + "SLODialectValidator": reflect.ValueOf((*validation.SLODialectValidator)(nil)), + + // interface wrapper definitions + "_SLODialectValidator": reflect.ValueOf((*_github_com_slok_sloth_pkg_common_validation_SLODialectValidator)(nil)), + } +} + +// _github_com_slok_sloth_pkg_common_validation_SLODialectValidator is an interface wrapper for SLODialectValidator type +type _github_com_slok_sloth_pkg_common_validation_SLODialectValidator struct { + IValue interface{} + WValidateAnnotationKey func(k string) error + WValidateAnnotationValue func(k string) error + WValidateLabelKey func(k string) error + WValidateLabelValue func(k string) error + WValidateQueryExpression func(queryExpression string) error +} + +func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateAnnotationKey(k string) error { + return W.WValidateAnnotationKey(k) +} +func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateAnnotationValue(k string) error { + return W.WValidateAnnotationValue(k) +} +func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateLabelKey(k string) error { + return W.WValidateLabelKey(k) +} +func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateLabelValue(k string) error { + return W.WValidateLabelValue(k) +} +func (W _github_com_slok_sloth_pkg_common_validation_SLODialectValidator) ValidateQueryExpression(queryExpression string) error { + return W.WValidateQueryExpression(queryExpression) +} diff --git a/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-prometheus-plugin-k8stransform-v1.go b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-prometheus-plugin-k8stransform-v1.go new file mode 100644 index 00000000..fe263a25 --- /dev/null +++ b/internal/pluginengine/k8stransform/custom/github_com-slok-sloth-pkg-prometheus-plugin-k8stransform-v1.go @@ -0,0 +1,42 @@ +// Code generated by 'yaegi extract github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1'. DO NOT EDIT. + +package custom + +import ( + "context" + "github.com/slok/sloth/pkg/common/model" + "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" + "go/constant" + "go/token" + "reflect" +) + +func init() { + Symbols["github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1/v1"] = map[string]reflect.Value{ + // function, constant and variable definitions + "PluginFactoryName": reflect.ValueOf(constant.MakeFromLiteral("\"NewPlugin\"", token.STRING, 0)), + "PluginIDName": reflect.ValueOf(constant.MakeFromLiteral("\"PluginID\"", token.STRING, 0)), + "PluginVersionName": reflect.ValueOf(constant.MakeFromLiteral("\"PluginVersion\"", token.STRING, 0)), + "Version": reflect.ValueOf(constant.MakeFromLiteral("\"prometheus/k8stransform/v1\"", token.STRING, 0)), + + // type definitions + "K8sObjects": reflect.ValueOf((*v1.K8sObjects)(nil)), + "Plugin": reflect.ValueOf((*v1.Plugin)(nil)), + "PluginFactory": reflect.ValueOf((*v1.PluginFactory)(nil)), + "PluginID": reflect.ValueOf((*v1.PluginID)(nil)), + "PluginVersion": reflect.ValueOf((*v1.PluginVersion)(nil)), + + // interface wrapper definitions + "_Plugin": reflect.ValueOf((*_github_com_slok_sloth_pkg_prometheus_plugin_k8stransform_v1_Plugin)(nil)), + } +} + +// _github_com_slok_sloth_pkg_prometheus_plugin_k8stransform_v1_Plugin is an interface wrapper for Plugin type +type _github_com_slok_sloth_pkg_prometheus_plugin_k8stransform_v1_Plugin struct { + IValue interface{} + WTransformK8sObjects func(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*v1.K8sObjects, error) +} + +func (W _github_com_slok_sloth_pkg_prometheus_plugin_k8stransform_v1_Plugin) TransformK8sObjects(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*v1.K8sObjects, error) { + return W.WTransformK8sObjects(ctx, kmeta, sloResult) +} diff --git a/internal/pluginengine/k8stransform/custom/k8s_io-apimachinery-pkg-apis-meta-v1-unstructured.go b/internal/pluginengine/k8stransform/custom/k8s_io-apimachinery-pkg-apis-meta-v1-unstructured.go new file mode 100644 index 00000000..251102c0 --- /dev/null +++ b/internal/pluginengine/k8stransform/custom/k8s_io-apimachinery-pkg-apis-meta-v1-unstructured.go @@ -0,0 +1,38 @@ +// Code generated by 'yaegi extract k8s.io/apimachinery/pkg/apis/meta/v1/unstructured'. DO NOT EDIT. + +package custom + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "reflect" +) + +func init() { + Symbols["k8s.io/apimachinery/pkg/apis/meta/v1/unstructured/unstructured"] = map[string]reflect.Value{ + // function, constant and variable definitions + "NestedBool": reflect.ValueOf(unstructured.NestedBool), + "NestedFieldCopy": reflect.ValueOf(unstructured.NestedFieldCopy), + "NestedFieldNoCopy": reflect.ValueOf(unstructured.NestedFieldNoCopy), + "NestedFloat64": reflect.ValueOf(unstructured.NestedFloat64), + "NestedInt64": reflect.ValueOf(unstructured.NestedInt64), + "NestedMap": reflect.ValueOf(unstructured.NestedMap), + "NestedNullCoercingStringMap": reflect.ValueOf(unstructured.NestedNullCoercingStringMap), + "NestedNumberAsFloat64": reflect.ValueOf(unstructured.NestedNumberAsFloat64), + "NestedSlice": reflect.ValueOf(unstructured.NestedSlice), + "NestedString": reflect.ValueOf(unstructured.NestedString), + "NestedStringMap": reflect.ValueOf(unstructured.NestedStringMap), + "NestedStringSlice": reflect.ValueOf(unstructured.NestedStringSlice), + "NewJSONFallbackEncoder": reflect.ValueOf(unstructured.NewJSONFallbackEncoder), + "RemoveNestedField": reflect.ValueOf(unstructured.RemoveNestedField), + "SetNestedField": reflect.ValueOf(unstructured.SetNestedField), + "SetNestedMap": reflect.ValueOf(unstructured.SetNestedMap), + "SetNestedSlice": reflect.ValueOf(unstructured.SetNestedSlice), + "SetNestedStringMap": reflect.ValueOf(unstructured.SetNestedStringMap), + "SetNestedStringSlice": reflect.ValueOf(unstructured.SetNestedStringSlice), + "UnstructuredJSONScheme": reflect.ValueOf(&unstructured.UnstructuredJSONScheme).Elem(), + + // type definitions + "Unstructured": reflect.ValueOf((*unstructured.Unstructured)(nil)), + "UnstructuredList": reflect.ValueOf((*unstructured.UnstructuredList)(nil)), + } +} diff --git a/internal/pluginengine/k8stransform/k8stransform.go b/internal/pluginengine/k8stransform/k8stransform.go new file mode 100644 index 00000000..ee0d9e36 --- /dev/null +++ b/internal/pluginengine/k8stransform/k8stransform.go @@ -0,0 +1,123 @@ +package slo + +import ( + "context" + "fmt" + "os" + "regexp" + + "github.com/traefik/yaegi/interp" + "github.com/traefik/yaegi/stdlib" + "github.com/traefik/yaegi/stdlib/unsafe" + + "github.com/slok/sloth/internal/pluginengine/k8stransform/custom" + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" +) + +type Plugin struct { + ID string + PluginK8sTransformV1 plugink8stransformv1.PluginFactory +} + +// PluginLoader knows how to load Go k8s transformer plugins using Yaegi. +const PluginLoader = pluginLoader(false) + +type pluginLoader bool + +var packageRegexp = regexp.MustCompile(`(?m)^package +([^\s]+) *$`) + +// LoadRawPlugin knows how to load plugins using Yaegi from source data not files, +// thats why, this implementation will not support any import library except standard +// library. +// +// The load process will search for: +// - A function called `NewPlugin` to obtain the plugin factory. +// - A constant called `PluginID` to obtain the plugin ID. +// - A constant called `PluginVersion` to obtain the plugin version. +func (p pluginLoader) LoadRawPlugin(ctx context.Context, src string) (*Plugin, error) { + // Load the plugin in a new interpreter. + // For each plugin we need to use an independent interpreter to avoid name collisions. + yaegiInterp, err := newYaeginInterpreter(true, true) + if err != nil { + return nil, fmt.Errorf("could not create a new Yaegi interpreter: %w", err) + } + + _, err = yaegiInterp.EvalWithContext(ctx, src) + if err != nil { + return nil, fmt.Errorf("could not evaluate plugin source code: %w", err) + } + + // Discover package name. + packageMatch := packageRegexp.FindStringSubmatch(src) + if len(packageMatch) != 2 { + return nil, fmt.Errorf("invalid plugin source code, could not get package name") + } + packageName := packageMatch[1] + + // Get plugin version and check if is a known one. + pluginVerTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.PluginVersion", packageName)) + if err != nil { + return nil, fmt.Errorf("could not get plugin version: %w", err) + } + + pluginVer, ok := pluginVerTmp.Interface().(plugink8stransformv1.PluginVersion) + if !ok || (pluginVer != plugink8stransformv1.Version) { + return nil, fmt.Errorf("unsuported plugin version: %s", pluginVer) + } + + // Get plugin ID. + pluginIDTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.PluginID", packageName)) + if err != nil { + return nil, fmt.Errorf("could not get plugin ID: %w", err) + } + + pluginID, ok := pluginIDTmp.Interface().(plugink8stransformv1.PluginID) + if !ok || pluginID == "" { + return nil, fmt.Errorf("invalid k8s transform plugin ID type") + } + + // Get plugin logic. + pluginFuncTmp, err := yaegiInterp.EvalWithContext(ctx, fmt.Sprintf("%s.%s", packageName, plugink8stransformv1.PluginFactoryName)) + if err != nil { + return nil, fmt.Errorf("could not get plugin: %w", err) + } + + plugin, ok := pluginFuncTmp.Interface().(plugink8stransformv1.PluginFactory) + if !ok { + return nil, fmt.Errorf("invalid k8s transform plugin type") + } + + return &Plugin{ + ID: pluginID, + PluginK8sTransformV1: plugin, + }, nil +} + +func newYaeginInterpreter(env, unrestricted bool) (*interp.Interpreter, error) { + envVars := []string{} + if env { + envVars = os.Environ() + } + i := interp.New(interp.Options{ + Env: envVars, + Unrestricted: unrestricted, + }) + err := i.Use(stdlib.Symbols) + if err != nil { + return nil, fmt.Errorf("could not use stdlib symbols: %w", err) + } + + // Add unsafe library. + err = i.Use(unsafe.Symbols) + if err != nil { + return nil, fmt.Errorf("yaegi could not use stdlib unsafe symbols: %w", err) + } + + // Add our own plugin library. + err = i.Use(custom.Symbols) + if err != nil { + return nil, fmt.Errorf("yaegi could not use custom symbols: %w", err) + } + + return i, nil +} diff --git a/internal/pluginengine/k8stransform/k8stransform_test.go b/internal/pluginengine/k8stransform/k8stransform_test.go new file mode 100644 index 00000000..41ce9e41 --- /dev/null +++ b/internal/pluginengine/k8stransform/k8stransform_test.go @@ -0,0 +1,250 @@ +package slo_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + pluginenginek8stransform "github.com/slok/sloth/internal/pluginengine/k8stransform" + "github.com/slok/sloth/pkg/common/model" + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" +) + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + pluginSrc string + execPlugin func(t *testing.T, p pluginenginek8stransform.Plugin) + expErr bool + }{ + "Empty plugin should fail.": { + pluginSrc: "", + execPlugin: func(t *testing.T, p pluginenginek8stransform.Plugin) {}, + expErr: true, + }, + + "An invalid plugin syntax should fail": { + pluginSrc: `package test{`, + execPlugin: func(t *testing.T, p pluginenginek8stransform.Plugin) {}, + expErr: true, + }, + + "A plugin without the required version, should fail.": { + pluginSrc: `package test +import ( + "context" + "encoding/json" + + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" +) + +const ( + PluginVersion = "prometheus/k8stransform/v2" + PluginID = "sloth.dev/noop/v1" +) + +func NewPlugin() (plugink8stransformv1.Plugin, error) { + return noopPlugin{}, nil +} + +type noopPlugin struct{} +func (noopPlugin) TransformK8sObjects(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*plugink8stransformv1.K8sObjects, error) { + return &plugink8stransformv1.K8sObjects{}, nil +} +`, + execPlugin: func(t *testing.T, p pluginenginek8stransform.Plugin) {}, + expErr: true, + }, + + "A plugin without the plugin ID, should fail.": { + pluginSrc: `package test +import ( + "context" + "encoding/json" + + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" +) + +const ( + PluginVersion = "prometheus/k8stransform/v1" + PluginID = "" +) + +func NewPlugin() (plugink8stransformv1.Plugin, error) { + return noopPlugin{}, nil +} + +type noopPlugin struct{} + +func (noopPlugin) TransformK8sObjects(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*plugink8stransformv1.K8sObjects, error) { + return &plugink8stransformv1.K8sObjects{}, nil +} +`, + execPlugin: func(t *testing.T, p pluginenginek8stransform.Plugin) {}, + expErr: true, + }, + + "A plugin without the plugin factory, should fail.": { + pluginSrc: `package test +import ( + "context" + "encoding/json" + + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" +) + +const ( + PluginVersion = "prometheus/k8stransform/v1" + PluginID = "sloth.dev/noop/v1" +) + +func NewPlugin2() (plugink8stransformv1.Plugin, error) { + return noopPlugin{}, nil +} + +type noopPlugin struct{} + +func (noopPlugin) TransformK8sObjects(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*plugink8stransformv1.K8sObjects, error) { + return &plugink8stransformv1.K8sObjects{}, nil +} +`, + execPlugin: func(t *testing.T, p pluginenginek8stransform.Plugin) {}, + expErr: true, + }, + + "A correct plugin should execute the plugin.": { + pluginSrc: `package plugin + +import ( + "context" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/slok/sloth/pkg/common/model" + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" +) + +const ( + PluginVersion = "prometheus/k8stransform/v1" + PluginID = "sloth.dev/test-transform/v1" +) + +func NewPlugin() (plugink8stransformv1.Plugin, error) { + return noopPlugin{}, nil +} + +type noopPlugin struct{} + +type m map[string]any + +func (noopPlugin) TransformK8sObjects(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*plugink8stransformv1.K8sObjects, error) { + objs := []*unstructured.Unstructured{ + { + Object: m{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": m{ + "name": sloResult.SLOResults[0].SLO.ID, + "namespace": "default", + }, + "spec": m{ + "containers": []m{ + { + "name": sloResult.SLOResults[0].SLO.ID, + "image": "nginx:latest", + }, + }, + }, + }, + }, + { + Object: m{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": m{ + "name": sloResult.SLOResults[0].SLO.ID + "-cfg", + "labels": m{ + "slo": "true", + }, + }, + "data": m{ + "description": sloResult.SLOResults[0].SLO.Description, + }, + }, + }, + } + return &plugink8stransformv1.K8sObjects{ + Items: objs, + }, nil +}`, + execPlugin: func(t *testing.T, p pluginenginek8stransform.Plugin) { + plugin, err := p.PluginK8sTransformV1() + require.NoError(t, err) + + sloResults := model.PromSLOGroupResult{ + SLOResults: []model.PromSLOResult{ + {SLO: model.PromSLO{ + ID: "my-slo", + Description: "My SLO description", + }}, + }, + } + kMeta := model.K8sMeta{} + objs, err := plugin.TransformK8sObjects(t.Context(), kMeta, sloResults) + require.NoError(t, err) + + expObjs := plugink8stransformv1.K8sObjects{ + Items: []*unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "name": "my-slo", + "namespace": "default", + }, + "spec": map[string]interface{}{ + "containers": []map[string]interface{}{ + { + "name": "my-slo", + "image": "nginx:latest", + }, + }, + }, + }, + }, + { + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "my-slo-cfg", + "labels": map[string]interface{}{ + "slo": "true", + }, + }, + "data": map[string]interface{}{ + "description": "My SLO description", + }, + }, + }, + }, + } + assert.Equal(t, expObjs, *objs) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + plugin, err := pluginenginek8stransform.PluginLoader.LoadRawPlugin(t.Context(), test.pluginSrc) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + test.execPlugin(t, *plugin) + } + }) + } +} diff --git a/internal/storage/fs/fsmock/mocks.go b/internal/storage/fs/fsmock/mocks.go index ff3ae6e0..55c20020 100644 --- a/internal/storage/fs/fsmock/mocks.go +++ b/internal/storage/fs/fsmock/mocks.go @@ -7,6 +7,7 @@ package fsmock import ( "context" + slo0 "github.com/slok/sloth/internal/pluginengine/k8stransform" "github.com/slok/sloth/internal/pluginengine/sli" "github.com/slok/sloth/internal/pluginengine/slo" mock "github.com/stretchr/testify/mock" @@ -201,3 +202,98 @@ func (_c *SLOPluginLoader_LoadRawPlugin_Call) RunAndReturn(run func(ctx context. _c.Call.Return(run) return _c } + +// NewK8sTransformPluginLoader creates a new instance of K8sTransformPluginLoader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewK8sTransformPluginLoader(t interface { + mock.TestingT + Cleanup(func()) +}) *K8sTransformPluginLoader { + mock := &K8sTransformPluginLoader{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// K8sTransformPluginLoader is an autogenerated mock type for the K8sTransformPluginLoader type +type K8sTransformPluginLoader struct { + mock.Mock +} + +type K8sTransformPluginLoader_Expecter struct { + mock *mock.Mock +} + +func (_m *K8sTransformPluginLoader) EXPECT() *K8sTransformPluginLoader_Expecter { + return &K8sTransformPluginLoader_Expecter{mock: &_m.Mock} +} + +// LoadRawPlugin provides a mock function for the type K8sTransformPluginLoader +func (_mock *K8sTransformPluginLoader) LoadRawPlugin(ctx context.Context, src string) (*slo0.Plugin, error) { + ret := _mock.Called(ctx, src) + + if len(ret) == 0 { + panic("no return value specified for LoadRawPlugin") + } + + var r0 *slo0.Plugin + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*slo0.Plugin, error)); ok { + return returnFunc(ctx, src) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *slo0.Plugin); ok { + r0 = returnFunc(ctx, src) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*slo0.Plugin) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, src) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// K8sTransformPluginLoader_LoadRawPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadRawPlugin' +type K8sTransformPluginLoader_LoadRawPlugin_Call struct { + *mock.Call +} + +// LoadRawPlugin is a helper method to define mock.On call +// - ctx context.Context +// - src string +func (_e *K8sTransformPluginLoader_Expecter) LoadRawPlugin(ctx interface{}, src interface{}) *K8sTransformPluginLoader_LoadRawPlugin_Call { + return &K8sTransformPluginLoader_LoadRawPlugin_Call{Call: _e.mock.On("LoadRawPlugin", ctx, src)} +} + +func (_c *K8sTransformPluginLoader_LoadRawPlugin_Call) Run(run func(ctx context.Context, src string)) *K8sTransformPluginLoader_LoadRawPlugin_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *K8sTransformPluginLoader_LoadRawPlugin_Call) Return(plugin *slo0.Plugin, err error) *K8sTransformPluginLoader_LoadRawPlugin_Call { + _c.Call.Return(plugin, err) + return _c +} + +func (_c *K8sTransformPluginLoader_LoadRawPlugin_Call) RunAndReturn(run func(ctx context.Context, src string) (*slo0.Plugin, error)) *K8sTransformPluginLoader_LoadRawPlugin_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/storage/fs/plugin.go b/internal/storage/fs/plugin.go index 1c3454b9..9dc47145 100644 --- a/internal/storage/fs/plugin.go +++ b/internal/storage/fs/plugin.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/slok/sloth/internal/log" + pluginenginek8stransform "github.com/slok/sloth/internal/pluginengine/k8stransform" pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" commonerrors "github.com/slok/sloth/pkg/common/errors" @@ -21,27 +22,35 @@ type SLOPluginLoader interface { LoadRawPlugin(ctx context.Context, src string) (*pluginengineslo.Plugin, error) } +type K8sTransformPluginLoader interface { + LoadRawPlugin(ctx context.Context, src string) (*pluginenginek8stransform.Plugin, error) +} + type FilePluginRepo struct { - fss []fs.FS - sloPluginLoader SLOPluginLoader - sliPluginLoader SLIPluginLoader - sloPluginCache map[string]pluginengineslo.Plugin - sliPluginCache map[string]pluginenginesli.SLIPlugin - logger log.Logger - mu sync.RWMutex - failOnError bool + fss []fs.FS + sloPluginLoader SLOPluginLoader + sliPluginLoader SLIPluginLoader + k8sTransformLoader K8sTransformPluginLoader + sloPluginCache map[string]pluginengineslo.Plugin + sliPluginCache map[string]pluginenginesli.SLIPlugin + k8sTransformCache map[string]pluginenginek8stransform.Plugin + logger log.Logger + mu sync.RWMutex + failOnError bool } // NewFilePluginRepo returns a new FilePluginRepo that loads SLI and SLO plugins from the given file system. -func NewFilePluginRepo(logger log.Logger, failOnError bool, sliPluginLoader SLIPluginLoader, sloPluginLoader SLOPluginLoader, fss ...fs.FS) (*FilePluginRepo, error) { +func NewFilePluginRepo(logger log.Logger, failOnError bool, sliPluginLoader SLIPluginLoader, sloPluginLoader SLOPluginLoader, k8sTransformLoader K8sTransformPluginLoader, fss ...fs.FS) (*FilePluginRepo, error) { r := &FilePluginRepo{ - fss: fss, - sliPluginLoader: sliPluginLoader, - sloPluginLoader: sloPluginLoader, - sloPluginCache: map[string]pluginengineslo.Plugin{}, - sliPluginCache: map[string]pluginenginesli.SLIPlugin{}, - logger: logger, - failOnError: failOnError, + fss: fss, + sliPluginLoader: sliPluginLoader, + sloPluginLoader: sloPluginLoader, + k8sTransformLoader: k8sTransformLoader, + sloPluginCache: map[string]pluginengineslo.Plugin{}, + sliPluginCache: map[string]pluginenginesli.SLIPlugin{}, + k8sTransformCache: map[string]pluginenginek8stransform.Plugin{}, + logger: logger, + failOnError: failOnError, } err := r.Reload(context.Background()) @@ -55,7 +64,7 @@ func NewFilePluginRepo(logger log.Logger, failOnError bool, sliPluginLoader SLIP var pluginNameRegex = regexp.MustCompile("plugin.go$") func (r *FilePluginRepo) Reload(ctx context.Context) error { - sloPlugins, sliPlugins, err := r.loadPlugins(ctx, r.fss...) + sloPlugins, sliPlugins, k8sTransformPlugins, err := r.loadPlugins(ctx, r.fss...) if err != nil { return fmt.Errorf("could not load plugins: %w", err) } @@ -64,9 +73,10 @@ func (r *FilePluginRepo) Reload(ctx context.Context) error { r.mu.Lock() r.sloPluginCache = sloPlugins r.sliPluginCache = sliPlugins + r.k8sTransformCache = k8sTransformPlugins r.mu.Unlock() - r.logger.WithValues(log.Kv{"slo-plugins": len(sloPlugins), "sli-plugins": len(sliPlugins)}).Infof("Plugins loaded") + r.logger.WithValues(log.Kv{"slo-plugins": len(sloPlugins), "sli-plugins": len(sliPlugins), "k8s-transform-plugins": len(k8sTransformPlugins)}).Infof("Plugins loaded") return nil } @@ -108,9 +118,29 @@ func (r *FilePluginRepo) ListSLIPlugins(ctx context.Context) (map[string]plugine return r.sliPluginCache, nil } -func (r *FilePluginRepo) loadPlugins(ctx context.Context, fss ...fs.FS) (map[string]pluginengineslo.Plugin, map[string]pluginenginesli.SLIPlugin, error) { +func (r *FilePluginRepo) GetK8sTransformPlugin(ctx context.Context, id string) (*pluginenginek8stransform.Plugin, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + p, ok := r.k8sTransformCache[id] + if !ok { + return nil, fmt.Errorf("plugin %q not found: %w", id, commonerrors.ErrNotFound) + } + + return &p, nil +} + +func (r *FilePluginRepo) ListK8sTransformPlugins(ctx context.Context) (map[string]pluginenginek8stransform.Plugin, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + return r.k8sTransformCache, nil +} + +func (r *FilePluginRepo) loadPlugins(ctx context.Context, fss ...fs.FS) (map[string]pluginengineslo.Plugin, map[string]pluginenginesli.SLIPlugin, map[string]pluginenginek8stransform.Plugin, error) { sloPlugins := map[string]pluginengineslo.Plugin{} sliPlugins := map[string]pluginenginesli.SLIPlugin{} + k8sTransformPlugins := map[string]pluginenginek8stransform.Plugin{} for _, f := range fss { err := fs.WalkDir(f, ".", func(path string, d fs.DirEntry, err error) error { @@ -155,7 +185,19 @@ func (r *FilePluginRepo) loadPlugins(ctx context.Context, fss ...fs.FS) (map[str return nil } - err = fmt.Errorf("could not load %q as SLI or SLO plugin: (SLI plugin error: %w | SLO plugin error: %w)", path, sliErr, sloErr) + // Try K8s transform plugin. + k8sTransformPlugin, k8sErr := r.k8sTransformLoader.LoadRawPlugin(ctx, pluginData) + if k8sErr == nil { + _, ok := k8sTransformPlugins[k8sTransformPlugin.ID] + if ok { + return fmt.Errorf("plugin %q already loaded", k8sTransformPlugin.ID) + } + k8sTransformPlugins[k8sTransformPlugin.ID] = *k8sTransformPlugin + r.logger.WithValues(log.Kv{"k8s-transform-plugin-id": k8sTransformPlugin.ID}).Debugf("K8s transform plugin discovered and loaded") + return nil + } + + err = fmt.Errorf("could not load %q as any kind of plugin: (SLI plugin error: %w | SLO plugin error: %w | K8s transform plugin error: %w)", path, sliErr, sloErr, k8sErr) if r.failOnError { return err } @@ -164,9 +206,9 @@ func (r *FilePluginRepo) loadPlugins(ctx context.Context, fss ...fs.FS) (map[str return nil }) if err != nil { - return nil, nil, fmt.Errorf("could not walk dir: %w", err) + return nil, nil, nil, fmt.Errorf("could not walk dir: %w", err) } } - return sloPlugins, sliPlugins, nil + return sloPlugins, sliPlugins, k8sTransformPlugins, nil } diff --git a/internal/storage/fs/plugin_test.go b/internal/storage/fs/plugin_test.go index b205abf0..1ff305b1 100644 --- a/internal/storage/fs/plugin_test.go +++ b/internal/storage/fs/plugin_test.go @@ -21,14 +21,15 @@ func TestFilePluginRepoListSLOPlugins(t *testing.T) { tests := map[string]struct { failOnError bool fss func() []fs.FS - mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) + mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) expPlugins map[string]pluginengineslo.Plugin expLoadErr bool expErr bool }{ "Having no files, should return empty list of plugins.": { - fss: func() []fs.FS { return nil }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) {}, + fss: func() []fs.FS { return nil }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { + }, expPlugins: map[string]pluginengineslo.Plugin{}, }, @@ -50,7 +51,7 @@ func TestFilePluginRepoListSLOPlugins(t *testing.T) { return []fs.FS{m1, m2, m3} }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(nil, fmt.Errorf("something")) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p3").Once().Return(nil, fmt.Errorf("something")) @@ -83,7 +84,7 @@ func TestFilePluginRepoListSLOPlugins(t *testing.T) { return []fs.FS{m1} }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(nil, fmt.Errorf("something")) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) @@ -102,13 +103,14 @@ func TestFilePluginRepoListSLOPlugins(t *testing.T) { return []fs.FS{m1} }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(nil, fmt.Errorf("something")) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) mslopl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) mslopl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + mk8stl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) }, expPlugins: map[string]pluginengineslo.Plugin{ "p1": {ID: "p1"}, @@ -124,13 +126,14 @@ func TestFilePluginRepoListSLOPlugins(t *testing.T) { return []fs.FS{m1} }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(nil, fmt.Errorf("something")) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) mslopl.On("LoadRawPlugin", mock.Anything, "p1").Once().Return(&pluginengineslo.Plugin{ID: "p1"}, nil) mslopl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) + mk8stl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) }, expLoadErr: true, }, @@ -142,10 +145,11 @@ func TestFilePluginRepoListSLOPlugins(t *testing.T) { mslopl := fsmock.NewSLOPluginLoader(t) mslipl := fsmock.NewSLIPluginLoader(t) - test.mock(mslopl, mslipl) + mk8stl := fsmock.NewK8sTransformPluginLoader(t) + test.mock(mslopl, mslipl, mk8stl) // Create repository and load plugins. - repo, err := storagefs.NewFilePluginRepo(log.Noop, test.failOnError, mslipl, mslopl, test.fss()...) + repo, err := storagefs.NewFilePluginRepo(log.Noop, test.failOnError, mslipl, mslopl, mk8stl, test.fss()...) if test.expLoadErr { assert.Error(err) return @@ -166,15 +170,16 @@ func TestFilePluginRepoGetSLOPlugin(t *testing.T) { tests := map[string]struct { pluginID string fss func() []fs.FS - mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) + mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) expPlugin pluginengineslo.Plugin expErr bool }{ "Having no files, should fail.": { pluginID: "test", fss: func() []fs.FS { return nil }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) {}, - expErr: true, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { + }, + expErr: true, }, "Getting a correct plugin, should return the plugin.": { @@ -185,7 +190,7 @@ func TestFilePluginRepoGetSLOPlugin(t *testing.T) { m["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} return []fs.FS{m} }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(nil, fmt.Errorf("something")) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) @@ -203,10 +208,11 @@ func TestFilePluginRepoGetSLOPlugin(t *testing.T) { mslopl := fsmock.NewSLOPluginLoader(t) mslipl := fsmock.NewSLIPluginLoader(t) - test.mock(mslopl, mslipl) + mk8stl := fsmock.NewK8sTransformPluginLoader(t) + test.mock(mslopl, mslipl, mk8stl) // Create repository and load plugins. - repo, err := storagefs.NewFilePluginRepo(log.Noop, false, mslipl, mslopl, test.fss()...) + repo, err := storagefs.NewFilePluginRepo(log.Noop, false, mslipl, mslopl, mk8stl, test.fss()...) require.NoError(err) plugin, err := repo.GetSLOPlugin(t.Context(), test.pluginID) @@ -223,14 +229,15 @@ func TestFilePluginRepoListSLIPlugins(t *testing.T) { tests := map[string]struct { failOnError bool fss func() []fs.FS - mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) + mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) expPlugins map[string]pluginenginesli.SLIPlugin expLoadErr bool expErr bool }{ "Having no files, should return empty list of plugins.": { - fss: func() []fs.FS { return nil }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) {}, + fss: func() []fs.FS { return nil }, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { + }, expPlugins: map[string]pluginenginesli.SLIPlugin{}, }, @@ -252,7 +259,7 @@ func TestFilePluginRepoListSLIPlugins(t *testing.T) { return []fs.FS{m1, m2, m3} }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(&pluginenginesli.SLIPlugin{ID: "p2"}, nil) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p3").Once().Return(&pluginenginesli.SLIPlugin{ID: "p3"}, nil) @@ -278,7 +285,7 @@ func TestFilePluginRepoListSLIPlugins(t *testing.T) { return []fs.FS{m1} }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) }, @@ -293,11 +300,11 @@ func TestFilePluginRepoListSLIPlugins(t *testing.T) { return []fs.FS{m1} }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) mslopl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) - + mk8stl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) }, expPlugins: map[string]pluginenginesli.SLIPlugin{ "p1": {ID: "p1"}, @@ -313,11 +320,11 @@ func TestFilePluginRepoListSLIPlugins(t *testing.T) { return []fs.FS{m1} }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) mslopl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) - + mk8stl.On("LoadRawPlugin", mock.Anything, "p2").Once().Return(nil, fmt.Errorf("something")) }, expLoadErr: true, }, @@ -329,10 +336,11 @@ func TestFilePluginRepoListSLIPlugins(t *testing.T) { mslopl := fsmock.NewSLOPluginLoader(t) mslipl := fsmock.NewSLIPluginLoader(t) - test.mock(mslopl, mslipl) + mk8stl := fsmock.NewK8sTransformPluginLoader(t) + test.mock(mslopl, mslipl, mk8stl) // Create repository and load plugins. - repo, err := storagefs.NewFilePluginRepo(log.Noop, test.failOnError, mslipl, mslopl, test.fss()...) + repo, err := storagefs.NewFilePluginRepo(log.Noop, test.failOnError, mslipl, mslopl, mk8stl, test.fss()...) if test.expLoadErr { assert.Error(err) return @@ -353,15 +361,16 @@ func TestFilePluginRepoGetSLIPlugin(t *testing.T) { tests := map[string]struct { pluginID string fss func() []fs.FS - mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) + mock func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) expPlugin pluginenginesli.SLIPlugin expErr bool }{ "Having no files, should fail.": { pluginID: "test", fss: func() []fs.FS { return nil }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) {}, - expErr: true, + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { + }, + expErr: true, }, "Getting a correct plugin, should return the plugin.": { @@ -372,7 +381,7 @@ func TestFilePluginRepoGetSLIPlugin(t *testing.T) { m["m1/pl2/plugin.go"] = &fstest.MapFile{Data: []byte("p2")} return []fs.FS{m} }, - mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader) { + mock: func(mslopl *fsmock.SLOPluginLoader, mslipl *fsmock.SLIPluginLoader, mk8stl *fsmock.K8sTransformPluginLoader) { mslipl.On("LoadRawSLIPlugin", mock.Anything, "p1").Once().Return(&pluginenginesli.SLIPlugin{ID: "p1"}, nil) mslipl.On("LoadRawSLIPlugin", mock.Anything, "p2").Once().Return(&pluginenginesli.SLIPlugin{ID: "p2"}, nil) }, @@ -387,10 +396,11 @@ func TestFilePluginRepoGetSLIPlugin(t *testing.T) { mslopl := fsmock.NewSLOPluginLoader(t) mslipl := fsmock.NewSLIPluginLoader(t) - test.mock(mslopl, mslipl) + mk8stl := fsmock.NewK8sTransformPluginLoader(t) + test.mock(mslopl, mslipl, mk8stl) // Create repository and load plugins. - repo, err := storagefs.NewFilePluginRepo(log.Noop, false, mslipl, mslopl, test.fss()...) + repo, err := storagefs.NewFilePluginRepo(log.Noop, false, mslipl, mslopl, mk8stl, test.fss()...) require.NoError(err) plugin, err := repo.GetSLIPlugin(t.Context(), test.pluginID) diff --git a/internal/storage/io/k8s_obj.go b/internal/storage/io/k8s_obj.go new file mode 100644 index 00000000..011f1338 --- /dev/null +++ b/internal/storage/io/k8s_obj.go @@ -0,0 +1,62 @@ +package io + +import ( + "context" + "fmt" + "io" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + + "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/pkg/common/model" + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" +) + +func NewIOWriterK8sObjectYAMLRepo(writer io.Writer, transformer plugink8stransformv1.Plugin, logger log.Logger) IOWriterK8sObjectYAMLRepo { + return IOWriterK8sObjectYAMLRepo{ + writer: writer, + encoder: json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil), + logger: logger.WithValues(log.Kv{"svc": "storage.io.IOWriterK8sObjectYAMLRepo"}), + transformer: transformer, + } +} + +// IOWriterK8sObjectYAMLRepo knows to store all the SLO rules (recordings and alerts) +// grouped in an IOWriter in Kubernetes K8sObject YAML format. +type IOWriterK8sObjectYAMLRepo struct { + writer io.Writer + encoder runtime.Encoder + logger log.Logger + transformer plugink8stransformv1.Plugin +} + +func (i IOWriterK8sObjectYAMLRepo) StoreSLOs(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) error { + k8sObjs, err := i.transformer.TransformK8sObjects(ctx, kmeta, slos) + if err != nil { + return fmt.Errorf("could not transform k8s objects using plugin: %w", err) + } + + for _, obj := range k8sObjs.Items { + obj := obj.DeepCopy() + + l := obj.GetLabels() + if l == nil { + l = map[string]string{} + } + l["app.kubernetes.io/component"] = "SLO" + l["app.kubernetes.io/managed-by"] = "sloth" + obj.SetLabels(l) + + _, err := i.writer.Write([]byte(yamlTopdisclaimer)) + if err != nil { + return fmt.Errorf("could not write top disclaimer: %w", err) + } + err = i.encoder.Encode(obj, i.writer) + if err != nil { + return fmt.Errorf("could encode k8s object: %w", err) + } + } + + return nil +} diff --git a/internal/storage/io/prometheus_operator_test.go b/internal/storage/io/k8s_obj_test.go similarity index 61% rename from internal/storage/io/prometheus_operator_test.go rename to internal/storage/io/k8s_obj_test.go index 09d9253a..91970665 100644 --- a/internal/storage/io/prometheus_operator_test.go +++ b/internal/storage/io/k8s_obj_test.go @@ -10,187 +10,25 @@ import ( "github.com/stretchr/testify/assert" "github.com/slok/sloth/internal/log" + k8stransformpromopv1 "github.com/slok/sloth/internal/plugin/k8stransform/prom_operator_prometheus_rule_v1" "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" ) -func TestIOWriterPrometheusOperatorYAMLRepo(t *testing.T) { +func TestIOWriterK8sObjectYAMLRepo(t *testing.T) { tests := map[string]struct { - k8sMeta model.K8sMeta - slos model.PromSLOGroupResult - expYAML string - expErr bool + transformerFunc func() plugink8stransformv1.Plugin + k8sMeta model.K8sMeta + slos model.PromSLOGroupResult + expYAML string + expErr bool }{ - "Having 0 SLO rules should fail.": { - k8sMeta: model.K8sMeta{}, - slos: model.PromSLOGroupResult{}, - expErr: true, - }, - - "Having 0 SLO rules generated should fail.": { - k8sMeta: model.K8sMeta{}, - slos: model.PromSLOGroupResult{ - SLOResults: []model.PromSLOResult{}, - }, - expErr: true, - }, - - "Having a single SLI recording rule should render correctly.": { - k8sMeta: model.K8sMeta{ - Name: "test-name", - Namespace: "test-ns", - Labels: map[string]string{"lk1": "lv1"}, - Annotations: map[string]string{"ak1": "av1"}, - }, - slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ - { - SLO: model.PromSLO{ID: "test1"}, - PrometheusRules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{ - Name: "sloth-slo-sli-recordings-test1", - Rules: []rulefmt.Rule{ - { - Record: "test:record", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - }, - }}, - }, - }, - }}, - expYAML: ` ---- -# Code generated by Sloth (dev): https://github.com/slok/sloth. -# DO NOT EDIT. - -apiVersion: monitoring.coreos.com/v1 -kind: PrometheusRule -metadata: - annotations: - ak1: av1 - labels: - app.kubernetes.io/component: SLO - app.kubernetes.io/managed-by: sloth - lk1: lv1 - name: test-name - namespace: test-ns -spec: - groups: - - name: sloth-slo-sli-recordings-test1 - rules: - - expr: test-expr - labels: - test-label: one - record: test:record -`, - }, - - "Having a single metadata recording rule should render correctly.": { - k8sMeta: model.K8sMeta{ - Name: "test-name", - Namespace: "test-ns", - Labels: map[string]string{"lk1": "lv1"}, - Annotations: map[string]string{"ak1": "av1"}, - }, - slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ - { - SLO: model.PromSLO{ID: "test1"}, - PrometheusRules: model.PromSLORules{ - MetadataRecRules: model.PromRuleGroup{ - Name: "sloth-slo-meta-recordings-test1", - Interval: 42 * time.Minute, - Rules: []rulefmt.Rule{ - { - Record: "test:record", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - }, - }}, - }, - }, - }}, - expYAML: ` ---- -# Code generated by Sloth (dev): https://github.com/slok/sloth. -# DO NOT EDIT. - -apiVersion: monitoring.coreos.com/v1 -kind: PrometheusRule -metadata: - annotations: - ak1: av1 - labels: - app.kubernetes.io/component: SLO - app.kubernetes.io/managed-by: sloth - lk1: lv1 - name: test-name - namespace: test-ns -spec: - groups: - - interval: 42m - name: sloth-slo-meta-recordings-test1 - rules: - - expr: test-expr - labels: - test-label: one - record: test:record -`, - }, - - "Having a single SLO alert rule should render correctly.": { - k8sMeta: model.K8sMeta{ - Name: "test-name", - Namespace: "test-ns", - Labels: map[string]string{"lk1": "lv1"}, - Annotations: map[string]string{"ak1": "av1"}, - }, - slos: model.PromSLOGroupResult{SLOResults: []model.PromSLOResult{ - { - SLO: model.PromSLO{ID: "test1"}, - PrometheusRules: model.PromSLORules{ - AlertRules: model.PromRuleGroup{ - Name: "sloth-slo-alerts-test1", - Rules: []rulefmt.Rule{ - { - Alert: "testAlert", - Expr: "test-expr", - Labels: map[string]string{"test-label": "one"}, - Annotations: map[string]string{"test-annot": "one"}, - }, - }}, - }, - }, - }}, - expYAML: ` ---- -# Code generated by Sloth (dev): https://github.com/slok/sloth. -# DO NOT EDIT. - -apiVersion: monitoring.coreos.com/v1 -kind: PrometheusRule -metadata: - annotations: - ak1: av1 - labels: - app.kubernetes.io/component: SLO - app.kubernetes.io/managed-by: sloth - lk1: lv1 - name: test-name - namespace: test-ns -spec: - groups: - - name: sloth-slo-alerts-test1 - rules: - - alert: testAlert - annotations: - test-annot: one - expr: test-expr - labels: - test-label: one -`, - }, - "Having a multiple SLO alert and recording rules should render correctly.": { + transformerFunc: func() plugink8stransformv1.Plugin { + p, _ := k8stransformpromopv1.NewPlugin() + return p + }, k8sMeta: model.K8sMeta{ Name: "test-name", Namespace: "test-ns", @@ -418,7 +256,7 @@ spec: assert := assert.New(t) var gotYAML bytes.Buffer - repo := io.NewIOWriterPrometheusOperatorYAMLRepo(&gotYAML, log.Noop) + repo := io.NewIOWriterK8sObjectYAMLRepo(&gotYAML, test.transformerFunc(), log.Noop) err := repo.StoreSLOs(context.TODO(), test.k8sMeta, test.slos) if test.expErr { diff --git a/internal/storage/io/prometheus_operator.go b/internal/storage/io/prometheus_operator.go deleted file mode 100644 index 903b3660..00000000 --- a/internal/storage/io/prometheus_operator.go +++ /dev/null @@ -1,52 +0,0 @@ -package io - -import ( - "bytes" - "context" - "fmt" - "io" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer/json" - - kubernetesmodelmap "github.com/slok/sloth/internal/kubernetes/modelmap" - "github.com/slok/sloth/internal/log" - "github.com/slok/sloth/pkg/common/model" -) - -func NewIOWriterPrometheusOperatorYAMLRepo(writer io.Writer, logger log.Logger) IOWriterPrometheusOperatorYAMLRepo { - return IOWriterPrometheusOperatorYAMLRepo{ - writer: writer, - encoder: json.NewYAMLSerializer(json.DefaultMetaFactory, nil, nil), - logger: logger.WithValues(log.Kv{"svc": "storage.io.IOWriterPrometheusOperatorYAMLRepo"}), - } -} - -// IOWriterPrometheusOperatorYAMLRepo knows to store all the SLO rules (recordings and alerts) -// grouped in an IOWriter in Kubernetes prometheus operator YAML format. -type IOWriterPrometheusOperatorYAMLRepo struct { - writer io.Writer - encoder runtime.Encoder - logger log.Logger -} - -func (i IOWriterPrometheusOperatorYAMLRepo) StoreSLOs(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) error { - rule, err := kubernetesmodelmap.MapModelToPrometheusOperator(ctx, kmeta, slos) - if err != nil { - return fmt.Errorf("could not map model to Prometheus operator CR: %w", err) - } - - var b bytes.Buffer - err = i.encoder.Encode(rule, &b) - if err != nil { - return fmt.Errorf("could encode prometheus operator object: %w", err) - } - - rulesYaml := writeYAMLTopDisclaimer(b.Bytes()) - _, err = i.writer.Write(rulesYaml) - if err != nil { - return fmt.Errorf("could not write top disclaimer: %w", err) - } - - return nil -} diff --git a/internal/storage/k8s/fake.go b/internal/storage/k8s/fake.go index 152b5283..a690f237 100644 --- a/internal/storage/k8s/fake.go +++ b/internal/storage/k8s/fake.go @@ -2,16 +2,20 @@ package k8s import ( "context" + "fmt" - monitoringclientsetfake "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" + fakediscovery "k8s.io/client-go/discovery/fake" + fakedynamic "k8s.io/client-go/dynamic/fake" + k8stesting "k8s.io/client-go/testing" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientsetfake "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/fake" + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" ) type FakeApiserverRepository struct { @@ -20,13 +24,40 @@ type FakeApiserverRepository struct { // NewFakeApiserverRepository returns a new Kubernetes Service that will fake Kubernetes operations // using fake clients. -func NewFakeApiserverRepository(logger log.Logger) FakeApiserverRepository { - return FakeApiserverRepository{ - ksvc: NewApiserverRepository( - slothclientsetfake.NewSimpleClientset(prometheusServiceLevelFakes...), - monitoringclientsetfake.NewSimpleClientset(), - logger), +func NewFakeApiserverRepository(logger log.Logger, k8sTransformPlugin plugink8stransformv1.Plugin) (*FakeApiserverRepository, error) { + // Setup fake dynamic client with ConfigMap scheme. + // Important: When adding new k8s transform plugins we need to add their + // resources to the fake discovery client to be able to fake them. + dynamicCli := fakedynamic.NewSimpleDynamicClient(runtime.NewScheme()) + fakeDiscovery := &fakediscovery.FakeDiscovery{Fake: &k8stesting.Fake{Resources: []*metav1.APIResourceList{ + { + GroupVersion: "v1", + APIResources: []metav1.APIResource{ + {Name: "configmaps", Namespaced: true, Kind: "ConfigMap"}, + }, + }, + { + GroupVersion: "monitoring.coreos.com/v1", + APIResources: []metav1.APIResource{ + {Name: "prometheusrules", Namespaced: true, Kind: "PrometheusRule"}, + }, + }, + }}} + + c, err := NewApiserverRepository(ApiserverRepositoryConfig{ + SlothCli: slothclientsetfake.NewSimpleClientset(prometheusServiceLevelFakes...), + DynamicCli: dynamicCli, + DiscoveryCli: fakeDiscovery, + K8sTransformPlugin: k8sTransformPlugin, + Logger: logger, + }) + if err != nil { + return nil, fmt.Errorf("could not create fake k8s apiserver repository: %w", err) } + + return &FakeApiserverRepository{ + ksvc: *c, + }, nil } func (r FakeApiserverRepository) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { diff --git a/internal/storage/k8s/k8s.go b/internal/storage/k8s/k8s.go index 00c29d26..8dff86af 100644 --- a/internal/storage/k8s/k8s.go +++ b/internal/storage/k8s/k8s.go @@ -5,32 +5,82 @@ import ( "fmt" "time" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - monitoringclientset "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" kubeerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/restmapper" - kubernetesmodelmap "github.com/slok/sloth/internal/kubernetes/modelmap" "github.com/slok/sloth/internal/log" "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientset "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned" + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" ) +type ApiserverRepositoryConfig struct { + SlothCli slothclientset.Interface + DynamicCli dynamic.Interface + DiscoveryCli discovery.DiscoveryInterface + K8sTransformPlugin plugink8stransformv1.Plugin + Logger log.Logger +} + type ApiserverRepository struct { - slothCli slothclientset.Interface - monitoringCli monitoringclientset.Interface - logger log.Logger + slothCli slothclientset.Interface + dynamicCli dynamic.Interface + restMapper meta.RESTMapper + k8sTransform plugink8stransformv1.Plugin + logger log.Logger +} + +func (c *ApiserverRepositoryConfig) defaults() error { + if c.SlothCli == nil { + return fmt.Errorf("SlothCli must be set") + } + + if c.K8sTransformPlugin == nil { + return fmt.Errorf("k8s transform plugin must be set") + } + + if c.DynamicCli == nil { + return fmt.Errorf("dynamic CLI must be set") + } + + if c.DiscoveryCli == nil { + return fmt.Errorf("discovery CLI must be set") + } + + if c.Logger == nil { + c.Logger = log.Noop + } + + return nil } // NewApiserverRepository returns a new Kubernetes Apiserver storage. -func NewApiserverRepository(slothCli slothclientset.Interface, monitoringCli monitoringclientset.Interface, logger log.Logger) ApiserverRepository { - return ApiserverRepository{ - slothCli: slothCli, - monitoringCli: monitoringCli, - logger: logger.WithValues(log.Kv{"service": "storage.k8s."}), +func NewApiserverRepository(config ApiserverRepositoryConfig) (*ApiserverRepository, error) { + err := config.defaults() + if err != nil { + return nil, fmt.Errorf("invalid config: %v", err) } + + apiGroupResources, err := restmapper.GetAPIGroupResources(config.DiscoveryCli) + if err != nil { + return nil, fmt.Errorf("failed to get API group resources: %w", err) + } + + return &ApiserverRepository{ + slothCli: config.SlothCli, + dynamicCli: config.DynamicCli, + k8sTransform: config.K8sTransformPlugin, + restMapper: restmapper.NewDiscoveryRESTMapper(apiGroupResources), + logger: config.Logger.WithValues(log.Kv{"service": "storage.k8s."}), + }, nil + } func (r ApiserverRepository) ListPrometheusServiceLevels(ctx context.Context, ns string, opts metav1.ListOptions) (*slothv1.PrometheusServiceLevelList, error) { @@ -64,53 +114,89 @@ func (r ApiserverRepository) EnsurePrometheusServiceLevelStatus(ctx context.Cont } func (r ApiserverRepository) StoreSLOs(ctx context.Context, kmeta model.K8sMeta, slos model.PromSLOGroupResult) error { - // Map to the Prometheus operator CRD. - rule, err := kubernetesmodelmap.MapModelToPrometheusOperator(ctx, kmeta, slos) - if err != nil { - return fmt.Errorf("could not map model to Prometheus operator CR: %w", err) - } - - // Add object reference. - rule.ObjectMeta.OwnerReferences = append(rule.ObjectMeta.OwnerReferences, metav1.OwnerReference{ + ownRef := metav1.OwnerReference{ Kind: "PrometheusServiceLevel", APIVersion: "sloth.slok.dev/v1", Name: slos.OriginalSource.K8sSlothV1.Name, UID: slos.OriginalSource.K8sSlothV1.UID, - }) + } - // Create on API server. - err = r.ensurePrometheusRule(ctx, rule) + // Transform to k8s objects. + k8sObjs, err := r.k8sTransform.TransformK8sObjects(ctx, kmeta, slos) if err != nil { - return fmt.Errorf("could not ensure Prometheus operator rule CR: %w", err) + return fmt.Errorf("could not transform to k8s objects: %w", err) + } + + // Add object reference. + for _, obj := range k8sObjs.Items { + ownRefs := obj.GetOwnerReferences() + ownRefs = append(ownRefs, ownRef) + obj.SetOwnerReferences(ownRefs) } + // Ensure k8s objects. + err = r.ensureK8sObjects(ctx, k8sObjs.Items) + if err != nil { + return fmt.Errorf("could not ensure k8s objects: %w", err) + } return nil } -func (r ApiserverRepository) ensurePrometheusRule(ctx context.Context, pr *monitoringv1.PrometheusRule) error { +func (r ApiserverRepository) ensureK8sObjects(ctx context.Context, objs []*unstructured.Unstructured) error { logger := r.logger.WithCtxValues(ctx) - pr = pr.DeepCopy() - stored, err := r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Get(ctx, pr.Name, metav1.GetOptions{}) - if err != nil { - if !kubeerrors.IsNotFound(err) { - return err + + for _, obj := range objs { + obj := obj.DeepCopy() + + l := obj.GetLabels() + if l == nil { + l = map[string]string{} } - _, err = r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Create(ctx, pr, metav1.CreateOptions{}) + l["app.kubernetes.io/component"] = "SLO" + l["app.kubernetes.io/managed-by"] = "sloth" + obj.SetLabels(l) + + gvk := obj.GroupVersionKind() + mapping, err := r.restMapper.RESTMapping(gvk.GroupKind(), gvk.Version) if err != nil { - return err + return fmt.Errorf("could not get GVR for GVK %v: %w", gvk, err) } - logger.Debugf("monitoringv1.PrometheusRule has been created") - return nil - } + // With the GVR get the proper cli. + gvr := mapping.Resource + namespace := obj.GetNamespace() + var resource dynamic.ResourceInterface + if namespace != "" { + resource = r.dynamicCli.Resource(gvr).Namespace(namespace) + } else { + resource = r.dynamicCli.Resource(gvr) + } - // Force overwrite. - pr.ObjectMeta.ResourceVersion = stored.ResourceVersion - _, err = r.monitoringCli.MonitoringV1().PrometheusRules(pr.Namespace).Update(ctx, pr, metav1.UpdateOptions{}) - if err != nil { - return err + // Try setting the resources (Create or override). + name := obj.GetName() + stored, err := resource.Get(ctx, name, metav1.GetOptions{}) + logger = logger.WithValues(log.Kv{"gvr": gvr.String(), "namespace": namespace, "name": name}) + + if err != nil { + if !kubeerrors.IsNotFound(err) { + return fmt.Errorf("could not get object %s/%s: %w", namespace, name, err) + } + _, err = resource.Create(ctx, obj, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("could not create object %s/%s: %w", namespace, name, err) + } + logger.Debugf("Resource has been created") + continue + } + + obj.SetResourceVersion(stored.GetResourceVersion()) + _, err = resource.Update(ctx, obj, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("could not update object %s/%s: %w", namespace, name, err) + } + + logger.Debugf("Resource has been overwritten") } - logger.Debugf("monitoringv1.PrometheusRule has been overwritten") return nil } diff --git a/internal/storage/k8s/k8s_test.go b/internal/storage/k8s/k8s_test.go index 7a30b435..3cbc28b5 100644 --- a/internal/storage/k8s/k8s_test.go +++ b/internal/storage/k8s/k8s_test.go @@ -5,310 +5,289 @@ import ( "testing" "time" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - monitoringclientsetfake "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" "github.com/prometheus/prometheus/model/rulefmt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" + fakediscovery "k8s.io/client-go/discovery/fake" + fakedynamic "k8s.io/client-go/dynamic/fake" + k8stesting "k8s.io/client-go/testing" "github.com/slok/sloth/internal/log" + pluginenginek8stransform "github.com/slok/sloth/internal/pluginengine/k8stransform" storagek8s "github.com/slok/sloth/internal/storage/k8s" "github.com/slok/sloth/pkg/common/model" slothv1 "github.com/slok/sloth/pkg/kubernetes/api/sloth/v1" slothclientsetfake "github.com/slok/sloth/pkg/kubernetes/gen/clientset/versioned/fake" ) -func TestApiserverRepositoryStoreSLOs(t *testing.T) { - tests := map[string]struct { - k8sMeta model.K8sMeta - slos model.PromSLOGroupResult - expPromOperatorRules []monitoringv1.PrometheusRule - expErr bool - }{ - "Having 0 SLO rules should fail.": { - k8sMeta: model.K8sMeta{}, - slos: model.PromSLOGroupResult{}, - expErr: true, +var originalSource = model.PromSLOGroupSource{ + K8sSlothV1: &slothv1.PrometheusServiceLevel{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + UID: types.UID("test-uid"), }, + }, +} - "Having 0 SLO rules generated should fail.": { - k8sMeta: model.K8sMeta{}, - slos: model.PromSLOGroupResult{ - SLOResults: []model.PromSLOResult{}, +var testPromSLOGroupResult = model.PromSLOGroupResult{ + OriginalSource: originalSource, + SLOResults: []model.PromSLOResult{ + { + SLO: model.PromSLO{ID: "testa"}, + PrometheusRules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-testa", + Rules: []rulefmt.Rule{ + { + Record: "test:record-a1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + }, + { + Record: "test:record-a2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + }, + }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-testa", + Rules: []rulefmt.Rule{ + { + Record: "test:record-a3", + Expr: "test-expr-a3", + Labels: map[string]string{"test-label": "a-3"}, + }, + { + Record: "test:record-a4", + Expr: "test-expr-a4", + Labels: map[string]string{"test-label": "a-4"}, + }, + }}, + AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-testa", + Interval: 15 * time.Minute, // Custom interval. + Rules: []rulefmt.Rule{ + { + Alert: "testAlertA1", + Expr: "test-expr-a1", + Labels: map[string]string{"test-label": "a-1"}, + Annotations: map[string]string{"test-annot": "a-1"}, + }, + { + Alert: "testAlertA2", + Expr: "test-expr-a2", + Labels: map[string]string{"test-label": "a-2"}, + Annotations: map[string]string{"test-annot": "a-2"}, + }, + }}, }, - expErr: true, }, - - "Having a mixed example of multiple SLOs and options should ensure them on k8s correctly.": { - k8sMeta: model.K8sMeta{ - Name: "test-name", - Namespace: "test-ns", - Labels: map[string]string{"lk1": "lv1"}, - Annotations: map[string]string{"ak1": "av1"}, - }, - slos: model.PromSLOGroupResult{ - OriginalSource: model.PromSLOGroupSource{ - K8sSlothV1: &slothv1.PrometheusServiceLevel{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - UID: types.UID("test-uid"), + { + SLO: model.PromSLO{ID: "testb"}, + PrometheusRules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{ + Name: "sloth-slo-sli-recordings-testb", + Rules: []rulefmt.Rule{ + { + Record: "test:record-b1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, }, - }, - }, - SLOResults: []model.PromSLOResult{ - { - SLO: model.PromSLO{ID: "testa"}, - PrometheusRules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{ - Name: "sloth-slo-sli-recordings-testa", - Rules: []rulefmt.Rule{ - { - Record: "test:record-a1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - }, - { - Record: "test:record-a2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - }, - }}, - MetadataRecRules: model.PromRuleGroup{ - Name: "sloth-slo-meta-recordings-testa", - Rules: []rulefmt.Rule{ - { - Record: "test:record-a3", - Expr: "test-expr-a3", - Labels: map[string]string{"test-label": "a-3"}, - }, - { - Record: "test:record-a4", - Expr: "test-expr-a4", - Labels: map[string]string{"test-label": "a-4"}, - }, - }}, - AlertRules: model.PromRuleGroup{ - Name: "sloth-slo-alerts-testa", - Interval: 15 * time.Minute, // Custom interval. - Rules: []rulefmt.Rule{ - { - Alert: "testAlertA1", - Expr: "test-expr-a1", - Labels: map[string]string{"test-label": "a-1"}, - Annotations: map[string]string{"test-annot": "a-1"}, - }, - { - Alert: "testAlertA2", - Expr: "test-expr-a2", - Labels: map[string]string{"test-label": "a-2"}, - Annotations: map[string]string{"test-annot": "a-2"}, - }, - }}, + }}, + MetadataRecRules: model.PromRuleGroup{ + Name: "sloth-slo-meta-recordings-testb", + Rules: []rulefmt.Rule{ + { + Record: "test:record-b2", + Expr: "test-expr-b2", + Labels: map[string]string{"test-label": "b-2"}, }, - }, - { - SLO: model.PromSLO{ID: "testb"}, - PrometheusRules: model.PromSLORules{ - SLIErrorRecRules: model.PromRuleGroup{ - Name: "sloth-slo-sli-recordings-testb", - Rules: []rulefmt.Rule{ - { - Record: "test:record-b1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - }, - }}, - MetadataRecRules: model.PromRuleGroup{ - Name: "sloth-slo-meta-recordings-testb", - Rules: []rulefmt.Rule{ - { - Record: "test:record-b2", - Expr: "test-expr-b2", - Labels: map[string]string{"test-label": "b-2"}, - }, - }}, - AlertRules: model.PromRuleGroup{ - Name: "sloth-slo-alerts-testb", - Rules: []rulefmt.Rule{ - { - Alert: "testAlertB1", - Expr: "test-expr-b1", - Labels: map[string]string{"test-label": "b-1"}, - Annotations: map[string]string{"test-annot": "b-1"}, - }, - }}, - ExtraRules: []model.PromRuleGroup{ - { - Name: "sloth-slo-extra-rules-000-testb", - Interval: 42 * time.Minute, - Rules: []rulefmt.Rule{ - { - Alert: "testAlertZ1", - Expr: "test-expr-z1", - Labels: map[string]string{"test-label": "z-1"}, - Annotations: map[string]string{"test-annot": "z-1"}, - }, - }}, - {}, // Should be skipped. - { - Name: "sloth-slo-extra-rules-001-testb", - Rules: []rulefmt.Rule{ - { - Alert: "testAlertZ2", - Expr: "test-expr-z2", - Labels: map[string]string{"test-label": "z-2"}, - Annotations: map[string]string{"test-annot": "z-2"}, - }, - { - Alert: "testAlertZ3", - Expr: "test-expr-z3", - Labels: map[string]string{"test-label": "z-3"}, - Annotations: map[string]string{"test-annot": "z-3"}, - }, - }, - }, - }, + }}, + AlertRules: model.PromRuleGroup{ + Name: "sloth-slo-alerts-testb", + Rules: []rulefmt.Rule{ + { + Alert: "testAlertB1", + Expr: "test-expr-b1", + Labels: map[string]string{"test-label": "b-1"}, + Annotations: map[string]string{"test-annot": "b-1"}, }, - }, - }}, - expPromOperatorRules: []monitoringv1.PrometheusRule{ - { - TypeMeta: metav1.TypeMeta{ - APIVersion: "monitoring.coreos.com/v1", - Kind: "PrometheusRule", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "test-name", - Namespace: "test-ns", - Labels: map[string]string{ - "lk1": "lv1", - "app.kubernetes.io/component": "SLO", - "app.kubernetes.io/managed-by": "sloth", - }, - Annotations: map[string]string{"ak1": "av1"}, - OwnerReferences: []metav1.OwnerReference{ + }}, + ExtraRules: []model.PromRuleGroup{ + { + Name: "sloth-slo-extra-rules-000-testb", + Interval: 42 * time.Minute, + Rules: []rulefmt.Rule{ { - Kind: "PrometheusServiceLevel", - APIVersion: "sloth.slok.dev/v1", - Name: "test-name", - UID: types.UID("test-uid"), + Alert: "testAlertZ1", + Expr: "test-expr-z1", + Labels: map[string]string{"test-label": "z-1"}, + Annotations: map[string]string{"test-annot": "z-1"}, }, - }, - }, - Spec: monitoringv1.PrometheusRuleSpec{ - Groups: []monitoringv1.RuleGroup{ + }}, + {}, // Should be skipped. + { + Name: "sloth-slo-extra-rules-001-testb", + Rules: []rulefmt.Rule{ { - Name: "sloth-slo-sli-recordings-testa", - Rules: []monitoringv1.Rule{ - { - Record: "test:record-a1", - Expr: intstr.FromString("test-expr-a1"), - Labels: map[string]string{"test-label": "a-1"}, - }, - { - Record: "test:record-a2", - Expr: intstr.FromString("test-expr-a2"), - Labels: map[string]string{"test-label": "a-2"}, - }, - }, + Alert: "testAlertZ2", + Expr: "test-expr-z2", + Labels: map[string]string{"test-label": "z-2"}, + Annotations: map[string]string{"test-annot": "z-2"}, }, { - Name: "sloth-slo-meta-recordings-testa", - Rules: []monitoringv1.Rule{ - { - Record: "test:record-a3", - Expr: intstr.FromString("test-expr-a3"), - Labels: map[string]string{"test-label": "a-3"}, - }, - { - Record: "test:record-a4", - Expr: intstr.FromString("test-expr-a4"), - Labels: map[string]string{"test-label": "a-4"}, - }, - }, + Alert: "testAlertZ3", + Expr: "test-expr-z3", + Labels: map[string]string{"test-label": "z-3"}, + Annotations: map[string]string{"test-annot": "z-3"}, }, - { - Name: "sloth-slo-alerts-testa", - Interval: &([]monitoringv1.Duration{monitoringv1.Duration("15m")}[0]), - Rules: []monitoringv1.Rule{ - { - Alert: "testAlertA1", - Expr: intstr.FromString("test-expr-a1"), - Labels: map[string]string{"test-label": "a-1"}, - Annotations: map[string]string{"test-annot": "a-1"}, - }, - { - Alert: "testAlertA2", - Expr: intstr.FromString("test-expr-a2"), - Labels: map[string]string{"test-label": "a-2"}, - Annotations: map[string]string{"test-annot": "a-2"}, - }, - }, + }, + }, + }, + }, + }, + }} + +func TestApiserverRepositoryStoreSLOsWithK8sTransformPlugins(t *testing.T) { + // Create a simple k8s transform plugin that creates configmaps as unstructured objects. + pluginSrc := `package plugin + +import ( + "context" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/slok/sloth/pkg/common/model" + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" +) + +const ( + PluginVersion = "prometheus/k8stransform/v1" + PluginID = "sloth.dev/test-transform-test/v1" +) + +func NewPlugin() (plugink8stransformv1.Plugin, error) { + return testPlugin{}, nil +} + +type testPlugin struct{} + +type m map[string]any + +func (testPlugin) TransformK8sObjects(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*plugink8stransformv1.K8sObjects, error) { + objs := []*unstructured.Unstructured{} + + // Create a ConfigMap for each SLO. + for _, slo := range sloResult.SLOResults { + // Convert labels and annotations to map[string]interface{}. + labels := m{} + for k, v := range kmeta.Labels { + labels[k] = v + } + annotations := m{} + for k, v := range kmeta.Annotations { + annotations[k] = v + } + + obj := &unstructured.Unstructured{ + Object: m{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": m{ + "name": kmeta.Name + "-" + slo.SLO.ID, + "namespace": kmeta.Namespace, + "labels": labels, + "annotations": annotations, + }, + "data": m{ + "slo_id": slo.SLO.ID, + }, + }, + } + objs = append(objs, obj) + } + + return &plugink8stransformv1.K8sObjects{ + Items: objs, + }, nil +}` + + tests := map[string]struct { + k8sMeta model.K8sMeta + slos model.PromSLOGroupResult + expObjs []unstructured.Unstructured + expErr bool + }{ + "Having multiple SLOs should create unstructured objects correctly.": { + k8sMeta: model.K8sMeta{ + Name: "test-name", + Namespace: "test-ns", + Labels: map[string]string{"lk1": "lv1"}, + Annotations: map[string]string{"ak1": "av1"}, + }, + slos: testPromSLOGroupResult, + expObjs: []unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test-name-testa", + "namespace": "test-ns", + "labels": map[string]interface{}{ + "lk1": "lv1", + "app.kubernetes.io/component": "SLO", + "app.kubernetes.io/managed-by": "sloth", }, - { - Name: "sloth-slo-sli-recordings-testb", - Rules: []monitoringv1.Rule{ - { - Record: "test:record-b1", - Expr: intstr.FromString("test-expr-b1"), - Labels: map[string]string{"test-label": "b-1"}, - }, - }, + "annotations": map[string]interface{}{ + "ak1": "av1", }, - { - Name: "sloth-slo-meta-recordings-testb", - Rules: []monitoringv1.Rule{ - { - Record: "test:record-b2", - Expr: intstr.FromString("test-expr-b2"), - Labels: map[string]string{"test-label": "b-2"}, - }, + "ownerReferences": []interface{}{ + map[string]interface{}{ + "apiVersion": "sloth.slok.dev/v1", + "kind": "PrometheusServiceLevel", + "name": "test-name", + "uid": "test-uid", }, }, - { - Name: "sloth-slo-alerts-testb", - Rules: []monitoringv1.Rule{ - { - Alert: "testAlertB1", - Expr: intstr.FromString("test-expr-b1"), - Labels: map[string]string{"test-label": "b-1"}, - Annotations: map[string]string{"test-annot": "b-1"}, - }, - }, + }, + "data": map[string]interface{}{ + "slo_id": "testa", + }, + }, + }, + { + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "test-name-testb", + "namespace": "test-ns", + "labels": map[string]interface{}{ + "lk1": "lv1", + "app.kubernetes.io/component": "SLO", + "app.kubernetes.io/managed-by": "sloth", }, - { - Name: "sloth-slo-extra-rules-000-testb", - Interval: &([]monitoringv1.Duration{monitoringv1.Duration("42m")}[0]), - Rules: []monitoringv1.Rule{ - { - Alert: "testAlertZ1", - Expr: intstr.FromString("test-expr-z1"), - Labels: map[string]string{"test-label": "z-1"}, - Annotations: map[string]string{"test-annot": "z-1"}, - }, - }, + "annotations": map[string]interface{}{ + "ak1": "av1", }, - { - Name: "sloth-slo-extra-rules-001-testb", - Rules: []monitoringv1.Rule{ - { - Alert: "testAlertZ2", - Expr: intstr.FromString("test-expr-z2"), - Labels: map[string]string{"test-label": "z-2"}, - Annotations: map[string]string{"test-annot": "z-2"}, - }, - { - Alert: "testAlertZ3", - Expr: intstr.FromString("test-expr-z3"), - Labels: map[string]string{"test-label": "z-3"}, - Annotations: map[string]string{"test-annot": "z-3"}, - }, + "ownerReferences": []interface{}{ + map[string]interface{}{ + "apiVersion": "sloth.slok.dev/v1", + "kind": "PrometheusServiceLevel", + "name": "test-name", + "uid": "test-uid", }, }, }, + "data": map[string]interface{}{ + "slo_id": "testb", + }, }, }, }, @@ -320,21 +299,44 @@ func TestApiserverRepositoryStoreSLOs(t *testing.T) { assert := assert.New(t) require := require.New(t) - // Change to NewClientset when https://github.com/kubernetes/kubernetes/issues/126850 fixed. + // Load the k8s transform plugin. + pluginFactory, err := pluginenginek8stransform.PluginLoader.LoadRawPlugin(context.TODO(), pluginSrc) + require.NoError(err) + + plugin, err := pluginFactory.PluginK8sTransformV1() + require.NoError(err) + + // Create fake clients. slothCLI := slothclientsetfake.NewSimpleClientset() - promOpCli := monitoringclientsetfake.NewSimpleClientset() - repo := storagek8s.NewApiserverRepository(slothCLI, promOpCli, log.Noop) - err := repo.StoreSLOs(context.TODO(), test.k8sMeta, test.slos) + // Setup fake dynamic client with ConfigMap scheme. + scheme := runtime.NewScheme() + err = corev1.AddToScheme(scheme) + require.NoError(err) + dynamicCli := fakedynamic.NewSimpleDynamicClient(scheme) + fakeDiscovery := &fakediscovery.FakeDiscovery{Fake: &k8stesting.Fake{Resources: []*metav1.APIResourceList{ + { + GroupVersion: "v1", + APIResources: []metav1.APIResource{{Name: "configmaps", Namespaced: true, Kind: "ConfigMap"}}, + }, + }}} + + repo, err := storagek8s.NewApiserverRepository(storagek8s.ApiserverRepositoryConfig{ + SlothCli: slothCLI, + DynamicCli: dynamicCli, + DiscoveryCli: fakeDiscovery, + K8sTransformPlugin: plugin, + Logger: log.Noop, + }) + require.NoError(err) + err = repo.StoreSLOs(context.TODO(), test.k8sMeta, test.slos) if test.expErr { assert.Error(err) } else if assert.NoError(err) { - gotPromRules, err := promOpCli.MonitoringV1().PrometheusRules("").List(t.Context(), metav1.ListOptions{}) + gotObjs, err := dynamicCli.Resource(corev1.SchemeGroupVersion.WithResource("configmaps")).Namespace("").List(context.TODO(), metav1.ListOptions{}) require.NoError(err) - - assert.Equal(test.expPromOperatorRules, gotPromRules.Items) - + assert.Equal(test.expObjs, gotObjs.Items) } }) } diff --git a/pkg/common/utils/k8s/k8s.go b/pkg/common/utils/k8s/k8s.go new file mode 100644 index 00000000..58d67ef6 --- /dev/null +++ b/pkg/common/utils/k8s/k8s.go @@ -0,0 +1,64 @@ +package k8s + +import ( + "fmt" + + "gopkg.in/yaml.v2" + + "github.com/slok/sloth/pkg/common/model" + promutils "github.com/slok/sloth/pkg/common/utils/prometheus" +) + +// UnstructuredToYAMLString converts an unstructured map to a YAML string. +// This is useful for creating YAML content in ConfigMap data fields or similar use cases. +func UnstructuredToYAMLString(data any) (string, error) { + yamlBytes, err := yaml.Marshal(data) + if err != nil { + return "", fmt.Errorf("could not marshal to YAML: %w", err) + } + return string(yamlBytes), nil +} + +// PromRuleGroupToUnstructuredPromOperator transforms a Prometheus rule group to a PromOperator unstructured rule map. +func PromRuleGroupToUnstructuredPromOperator(p model.PromRuleGroup) map[string]any { + rules := []any{} // Be aware, unstructured wants []any not []map[string]any. + for _, rule := range p.Rules { + r := map[string]any{ + "expr": rule.Expr, + } + + if rule.Record != "" { + r["record"] = rule.Record + } + + if rule.Alert != "" { + r["alert"] = rule.Alert + } + + if len(rule.Labels) > 0 { + r["labels"] = mapStringStringToMapStringAny(rule.Labels) + } + if len(rule.Annotations) > 0 { + r["annotations"] = mapStringStringToMapStringAny(rule.Annotations) + } + + rules = append(rules, r) + } + r := map[string]any{ + "name": p.Name, + "rules": rules, + } + if p.Interval != 0 { + r["interval"] = promutils.TimeDurationToPromStr(p.Interval) + } + + return r +} + +func mapStringStringToMapStringAny(in map[string]string) map[string]any { + out := make(map[string]any) + for k, v := range in { + out[k] = v + } + return out +} diff --git a/pkg/common/utils/k8s/k8s_test.go b/pkg/common/utils/k8s/k8s_test.go new file mode 100644 index 00000000..e6738e45 --- /dev/null +++ b/pkg/common/utils/k8s/k8s_test.go @@ -0,0 +1,86 @@ +package k8s_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/slok/sloth/pkg/common/utils/k8s" +) + +func TestUnstructuredToYAMLString(t *testing.T) { + tests := map[string]struct { + data map[string]any + expYAML string + expErr bool + }{ + "Empty map should return empty YAML": { + data: map[string]any{}, + expYAML: "{}\n", + }, + "Simple map should be marshaled correctly": { + data: map[string]any{ + "name": "test", + "age": 42, + }, + expYAML: "age: 42\nname: test\n", + }, + "Nested map should be marshaled correctly": { + data: map[string]any{ + "metadata": map[string]any{ + "name": "test-name", + "namespace": "test-ns", + }, + "data": map[string]any{ + "key1": "value1", + "key2": "value2", + }, + }, + expYAML: `data: + key1: value1 + key2: value2 +metadata: + name: test-name + namespace: test-ns +`, + }, + "Map with slice should be marshaled correctly": { + data: map[string]any{ + "groups": []any{ + map[string]any{ + "name": "group1", + "rules": []any{ + map[string]any{ + "record": "rec1", + "expr": "exp1", + }, + }, + }, + }, + }, + expYAML: `groups: +- name: group1 + rules: + - expr: exp1 + record: rec1 +`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + yamlStr, err := k8s.UnstructuredToYAMLString(test.data) + + if test.expErr { + require.Error(err) + } else { + require.NoError(err) + assert.Equal(test.expYAML, yamlStr) + } + }) + } +} diff --git a/pkg/lib/gen.go b/pkg/lib/gen.go index 1d96b614..d518e1db 100644 --- a/pkg/lib/gen.go +++ b/pkg/lib/gen.go @@ -12,6 +12,7 @@ import ( "github.com/slok/sloth/internal/alert" "github.com/slok/sloth/internal/app/generate" "github.com/slok/sloth/internal/info" + k8stransformpromopv1 "github.com/slok/sloth/internal/plugin/k8stransform/prom_operator_prometheus_rule_v1" storagefs "github.com/slok/sloth/internal/storage/fs" storageio "github.com/slok/sloth/internal/storage/io" "github.com/slok/sloth/pkg/common/model" @@ -312,6 +313,20 @@ func (p PrometheusSLOGenerator) WriteResultAsPrometheusStd(ctx context.Context, // WriteResultAsK8sPrometheusOperator writes the SLO results into the writer as a Prometheus Operator CRD file. // More information in: https://prometheus-operator.dev/docs/api-reference/api/#monitoring.coreos.com/v1.PrometheusRule. func (p PrometheusSLOGenerator) WriteResultAsK8sPrometheusOperator(ctx context.Context, k8sMeta model.K8sMeta, slo model.PromSLOGroupResult, w io.Writer) error { - repo := storageio.NewIOWriterPrometheusOperatorYAMLRepo(w, p.logger) + return p.WriteResultAsK8sObjects(ctx, k8stransformpromopv1.PluginID, k8sMeta, slo, w) +} + +// WriteResultAsK8sObjects writes the SLO results into the writer as Kubernetes objects using a custom K8s transformer plugin. +func (p PrometheusSLOGenerator) WriteResultAsK8sObjects(ctx context.Context, k8sTransformerPluginID string, k8sMeta model.K8sMeta, slo model.PromSLOGroupResult, w io.Writer) error { + pluginFactory, err := p.pluginsRepo.GetK8sTransformPlugin(ctx, k8sTransformerPluginID) + if err != nil { + return fmt.Errorf("could not get k8s transformer plugin %q: %w", k8sTransformerPluginID, err) + } + plugin, err := pluginFactory.PluginK8sTransformV1() + if err != nil { + return fmt.Errorf("could not create k8s transformer plugin %q: %w", k8sTransformerPluginID, err) + } + + repo := storageio.NewIOWriterK8sObjectYAMLRepo(w, plugin, p.logger) return repo.StoreSLOs(ctx, k8sMeta, slo) } diff --git a/pkg/lib/helper.go b/pkg/lib/helper.go index 0ccce2fd..366c768e 100644 --- a/pkg/lib/helper.go +++ b/pkg/lib/helper.go @@ -12,6 +12,7 @@ import ( plugincoremetadatarulesv1 "github.com/slok/sloth/internal/plugin/slo/core/metadata_rules_v1" plugincoreslirulesv1 "github.com/slok/sloth/internal/plugin/slo/core/sli_rules_v1" plugincorevalidatev1 "github.com/slok/sloth/internal/plugin/slo/core/validate_v1" + pluginenginesk8stransform "github.com/slok/sloth/internal/pluginengine/k8stransform" pluginenginesli "github.com/slok/sloth/internal/pluginengine/sli" pluginengineslo "github.com/slok/sloth/internal/pluginengine/slo" storagefs "github.com/slok/sloth/internal/storage/fs" @@ -21,10 +22,10 @@ func createPluginLoader(ctx context.Context, logger log.Logger, pluginsFS []fs.F // We should load at least the Sloth embedded default ones. fss := append([]fs.FS{}, pluginsFS...) if len(fss) == 0 { - fss = append(fss, plugin.EmbeddedDefaultSLOPlugins) + fss = append(fss, plugin.EmbeddedDefaultSLOPlugins, plugin.EmbeddedDefaultK8sTransformPlugins) } - pluginsRepo, err := storagefs.NewFilePluginRepo(logger, strict, pluginenginesli.PluginLoader, pluginengineslo.PluginLoader, fss...) + pluginsRepo, err := storagefs.NewFilePluginRepo(logger, strict, pluginenginesli.PluginLoader, pluginengineslo.PluginLoader, pluginenginesk8stransform.PluginLoader, fss...) if err != nil { return nil, fmt.Errorf("could not create file SLO and SLI plugins repository: %w", err) } diff --git a/pkg/lib/lib_as_cli_use_cases_test.go b/pkg/lib/lib_as_cli_use_cases_test.go index bdb5dce1..fb57683a 100644 --- a/pkg/lib/lib_as_cli_use_cases_test.go +++ b/pkg/lib/lib_as_cli_use_cases_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/slok/sloth/internal/info" + "github.com/slok/sloth/internal/plugin" "github.com/slok/sloth/pkg/common/model" "github.com/slok/sloth/pkg/lib" "github.com/stretchr/testify/assert" @@ -20,7 +21,10 @@ import ( // core SLO generator logic is the same (the CLI uses the public library under the hood). func TestLibAsCLIIntegration(t *testing.T) { testWindowsFS := os.DirFS("../../test/integration/prometheus/windows") - testPluginsFS := os.DirFS("../../test/integration/prometheus/plugins") + testPluginsFS := []fs.FS{ + os.DirFS("../../test/integration/prometheus/plugins"), + plugin.EmbeddedDefaultK8sTransformPlugins, + } tests := map[string]struct { config func() lib.PrometheusSLOGeneratorConfig @@ -175,7 +179,7 @@ func TestLibAsCLIIntegration(t *testing.T) { config: func() lib.PrometheusSLOGeneratorConfig { return lib.PrometheusSLOGeneratorConfig{ CallerAgent: lib.CallerAgentCLI, - PluginsFS: []fs.FS{testPluginsFS}, + PluginsFS: testPluginsFS, } }, inFilePath: "../../test/integration/prometheus/testdata/in-sli-plugin.yaml", @@ -192,7 +196,7 @@ func TestLibAsCLIIntegration(t *testing.T) { config: func() lib.PrometheusSLOGeneratorConfig { return lib.PrometheusSLOGeneratorConfig{ CallerAgent: lib.CallerAgentCLI, - PluginsFS: []fs.FS{testPluginsFS}, + PluginsFS: testPluginsFS, } }, inFilePath: "../../test/integration/prometheus/testdata/in-slo-plugin.yaml", @@ -209,7 +213,7 @@ func TestLibAsCLIIntegration(t *testing.T) { config: func() lib.PrometheusSLOGeneratorConfig { return lib.PrometheusSLOGeneratorConfig{ CallerAgent: lib.CallerAgentCLI, - PluginsFS: []fs.FS{testPluginsFS}, + PluginsFS: testPluginsFS, } }, inFilePath: "../../test/integration/prometheus/testdata/in-slo-plugin-k8s.yaml", diff --git a/pkg/prometheus/plugin/k8stransform/testing/testing.go b/pkg/prometheus/plugin/k8stransform/testing/testing.go new file mode 100644 index 00000000..d6a5a5ce --- /dev/null +++ b/pkg/prometheus/plugin/k8stransform/testing/testing.go @@ -0,0 +1,68 @@ +package testing + +import ( + "bytes" + "context" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + pluginenginek8stransform "github.com/slok/sloth/internal/pluginengine/k8stransform" + storageio "github.com/slok/sloth/internal/storage/io" + "github.com/slok/sloth/pkg/common/model" + "github.com/slok/sloth/pkg/lib/log" + plugink8stransformv1 "github.com/slok/sloth/pkg/prometheus/plugin/k8stransform/v1" +) + +// PluginTester is a helper util to load a plugin using the engine that +// will use Sloth. In the sense of an acceptance/integration test. +// +// This has benefits over loading the plugin directly with Go, by using this method +// you will be sure that what is executed is what the sloth will execute at runtime, +// so, if you use a not supported feature or the engine has a bug, this will be +// detected on the tests instead of Sloth runtime on execution. +type PluginTester struct { + plugin plugink8stransformv1.Plugin +} + +func NewPluginTester(pluginPath string) (*PluginTester, error) { + if pluginPath == "" { + pluginPath = "./plugin.go" + } + + pluginSource, err := os.ReadFile(pluginPath) + if err != nil { + return nil, fmt.Errorf("could not read plugin source code: %w", err) + } + + pluginFact, err := pluginenginek8stransform.PluginLoader.LoadRawPlugin(context.Background(), string(pluginSource)) + if err != nil { + return nil, fmt.Errorf("could not load plugin source code: %w", err) + } + + plugin, err := pluginFact.PluginK8sTransformV1() + if err != nil { + return nil, fmt.Errorf("could not create plugin instance: %w", err) + } + + return &PluginTester{ + plugin: plugin, + }, nil +} + +// AssertYAML asserts that the given SLOs when transformed by the plugin +// produce the expected YAML output. +func (p *PluginTester) AssertYAML(t *testing.T, expYAML string, kmeta model.K8sMeta, slos model.PromSLOGroupResult) { + var b bytes.Buffer + repo := storageio.NewIOWriterK8sObjectYAMLRepo(&b, p.plugin, log.Noop) + + err := repo.StoreSLOs(t.Context(), kmeta, slos) + if err != nil { + assert.NoError(t, err) + return + } + + assert.Equal(t, expYAML, b.String()) +} diff --git a/pkg/prometheus/plugin/k8stransform/v1/v1.go b/pkg/prometheus/plugin/k8stransform/v1/v1.go new file mode 100644 index 00000000..8c767b4a --- /dev/null +++ b/pkg/prometheus/plugin/k8stransform/v1/v1.go @@ -0,0 +1,37 @@ +package v1 + +import ( + "context" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/slok/sloth/pkg/common/model" +) + +// Version is this plugin type version. +const Version = "prometheus/k8stransform/v1" + +// PluginVersion is the version of the plugin (e.g: `prometheus/k8stransform/v1`). +type PluginVersion = string + +const PluginVersionName = "PluginVersion" + +// PluginID is the ID of the plugin (e.g: sloth.dev/my-test-plugin/v1). +type PluginID = string + +const PluginIDName = "PluginID" + +type K8sObjects struct { + Items []*unstructured.Unstructured +} + +// PluginFactoryName is the required name for the plugin factory. +const PluginFactoryName = "NewPlugin" + +type PluginFactory = func() (Plugin, error) + +// Plugin knows how to transform K8s objects, these transformers should be simple and +// only focused on transforming K8s objects generated from SLOs. +type Plugin interface { + TransformK8sObjects(ctx context.Context, kmeta model.K8sMeta, sloResult model.PromSLOGroupResult) (*K8sObjects, error) +} From 1ea462a655d55a570b7333ff4b5db6ae8a1b3d10 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sun, 2 Nov 2025 07:48:29 +0100 Subject: [PATCH 112/173] Update changelog Signed-off-by: Xabier Larrakoetxea --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc95cd30..64fb5669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,11 @@ - K8s transformer plugins to be able to customize the k8s resulting objects without depending on current Prometheys operator Rule CR. - `sloth.dev/k8stransform/prom-operator-prometheus-rule/v1` K8s transformer plugin. - Users can now create multiple K8s objects as the output of the SLO generated rules. +- Sloth lib support for K8s transformer plugins using `WriteResultAsK8sObjects`. ### Changed -- By default sloth now uses a dynamic `unstructured` plugin (`sloth.dev/k8stransform/prom-operator-prometheus-rule/v1`) to create and manager the prometheus operator Rule K8s CRs. +- Sloth now uses a dynamic `unstructured` plugin (`sloth.dev/k8stransform/prom-operator-prometheus-rule/v1`) to create and manage the prometheus operator Rule K8s CRs. ## [v0.15.0] - 2025-10-31 From 8b8c9c881388aafb238894ec35a47d86bbce14b5 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Tue, 4 Nov 2025 22:42:42 +0100 Subject: [PATCH 113/173] Use the k8stransform plugin on CLI execution Signed-off-by: Xabier Larrakoetxea --- cmd/sloth/commands/generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sloth/commands/generate.go b/cmd/sloth/commands/generate.go index b75fbd70..8f1695cb 100644 --- a/cmd/sloth/commands/generate.go +++ b/cmd/sloth/commands/generate.go @@ -286,7 +286,7 @@ func (g generateCommand) storeSLOs(ctx context.Context, logger log.Logger, gener Annotations: genResult.OriginalSource.K8sSlothV1.Annotations, } - return generator.WriteResultAsK8sPrometheusOperator(ctx, kmeta, genResult, out) + return generator.WriteResultAsK8sObjects(ctx, g.k8sTransformPluginID, kmeta, genResult, out) // OpenSLO. case genResult.OriginalSource.OpenSLOV1Alpha != nil: From 499dfe019e3ca67f43d11103f870cf67540f7d09 Mon Sep 17 00:00:00 2001 From: Adam Fox Date: Sat, 8 Nov 2025 16:03:41 +1030 Subject: [PATCH 114/173] feat: add remove_labels_v1 SLO plugin --- .../slo/contrib/remove_labels_v1/README.md | 80 ++++ .../slo/contrib/remove_labels_v1/plugin.go | 76 ++++ .../contrib/remove_labels_v1/plugin_test.go | 415 ++++++++++++++++++ 3 files changed, 571 insertions(+) create mode 100644 internal/plugin/slo/contrib/remove_labels_v1/README.md create mode 100644 internal/plugin/slo/contrib/remove_labels_v1/plugin.go create mode 100644 internal/plugin/slo/contrib/remove_labels_v1/plugin_test.go diff --git a/internal/plugin/slo/contrib/remove_labels_v1/README.md b/internal/plugin/slo/contrib/remove_labels_v1/README.md new file mode 100644 index 00000000..b1afb52b --- /dev/null +++ b/internal/plugin/slo/contrib/remove_labels_v1/README.md @@ -0,0 +1,80 @@ +# sloth.dev/contrib/remove_labels/v1 + +This SLO plugin removes custom labels from all SLI and metadata metrics except the `sloth_slo_info` metric. It will preserve +the SLO ID labels (`sloth_service`, `sloth_slo`, `sloth_id`), the `sloth_window` label on SLI metrics, and any other labels +specified by the `preserveLabels` config option. As well as not removing labels from the `sloth_slo_info` metric, it will +also not remove labels from any metric specified by the `skipMetrics` config option. + +## Motivation + +Sloth allows adding labels to the SLI and metadata metrics it generates by specifying `labels:` in the Sloth specification. +This provides a convenient mechanism to add extra metadata to SLO metrics, but when using the default optimized 30 day +recording rule any changes to these custom labels causes the 30 day recording rule to fail. This issue is described in +[Changing labels breaks the recording rule #311](https://github.com/slok/sloth/issues/311). + +If the values of custom labels change frequently, then the 30 day recording rule and all the error budget metadata metrics +will fail for up to 30 days just as frequently. An alternative is to only store these metadata labels on the `sloth_slo_info` +metric. + +One approach is to use the `info_labels_v1` SLO plugin which allows adding metadata labels to the `sloth_slo_info` metric. +This requires adding extra `plugin:` configuration stanzas in the Sloth specification though, which affects the readability +of the Sloth specification, and requires Sloth specifications to be updated. + +This `remove_labels_v1` SLO plugin provides another approach, by removing custom labels from SLO and metadata metrics. If +added as an application-level SLO plugin (i.e. using the `--slo-plugins` command line option), no changes to existing Sloth +specifications are required. + +### PromQL Query Methods + +If you have been using custom labels in your Sloth specifications, you would have had the convenience of being able to query +Sloth SLO metrics using these labels direct on the Sloth SLO metrics. For example: + +* `slo:period_error_budget_remaining:ratio{team="observability"}` + +If this `team` label is only present on the `sloth_slo_info` metric due to it being removed by this `remove_labels_v1` plugin, +you can still query for `slo:period_error_budget_remaining:ratio` metrics that belong to team observability by using the `and` +operator: + +* `slo:period_error_budget_remaining:ratio and on (sloth_id) sloth_slo_info{team="observability"}` + +Similarly, if you wanted to include the value of the `team` label in a PromQL query result you can use the `*` operator with +`group_left`: + +* `slo:period_error_budget_remaining:ratio * on (sloth_id) group_left(team) sloth_slo_info` + +## Configuration + +- `preserveLabels` (**Optional**, `[]string`): A list of labels to not remove from SLI and metadata metrics +- `skipMetrics` (**Optional**, `[]string`): A list of metrics to not remove labels from, in addition to `sloth_slo_info`. + +## Env vars + +None + +## Order Requirement + +This plugin should run after SLI and metadata rule generation plugins. + +## Usage Examples + +Add as a command line argument to `sloth`: + +```shell +sloth generate --plugins-path=/plugins --slo-plugins='{"id": "sloth.dev/contrib/remove_labels/v1", "priority": 100, "config": {"preserveLabels": ["namespace"]}}' ... +``` + +SLO plugin chain configuration: + +```yaml +chain: + - id: "sloth.dev/contrib/remove_labels/v1" + priority: 100 +``` + +```yaml +chain: + - id: "sloth.dev/contrib/remove_labels/v1" + priority: 100 + config: + preserveLabels: ["namespace"] +``` diff --git a/internal/plugin/slo/contrib/remove_labels_v1/plugin.go b/internal/plugin/slo/contrib/remove_labels_v1/plugin.go new file mode 100644 index 00000000..2eca84f4 --- /dev/null +++ b/internal/plugin/slo/contrib/remove_labels_v1/plugin.go @@ -0,0 +1,76 @@ +package plugin + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/slok/sloth/pkg/common/conventions" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" +) + +const ( + PluginVersion = "prometheus/slo/v1" + PluginID = "sloth.dev/contrib/remove_labels/v1" +) + +type Config struct { + PreserveLabels []string `json:"preserveLabels,omitempty"` + SkipMetrics []string `json:"skipMetrics,omitempty"` +} + +type plugin struct { + config Config +} + +func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1.Plugin, error) { + config := Config{} + err := json.Unmarshal(configData, &config) + if err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + return plugin{config: config}, nil +} + +func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { + preserveLabels := map[string]bool{"sloth_window": true} + for k := range conventions.GetSLOIDPromLabels(request.SLO) { + preserveLabels[k] = true + } + for _, k := range p.config.PreserveLabels { + preserveLabels[k] = true + } + + skipMetrics := map[string]bool{conventions.PromMetaSLOInfoMetric: true} + for _, k := range p.config.SkipMetrics { + skipMetrics[k] = true + } + + for i := range result.SLORules.SLIErrorRecRules.Rules { + if skipMetrics[result.SLORules.SLIErrorRecRules.Rules[i].Record] { + continue + } + result.SLORules.SLIErrorRecRules.Rules[i].Labels = removeLabels(result.SLORules.SLIErrorRecRules.Rules[i].Labels, preserveLabels) + } + + delete(preserveLabels, "sloth_window") + for i := range result.SLORules.MetadataRecRules.Rules { + if skipMetrics[result.SLORules.MetadataRecRules.Rules[i].Record] { + continue + } + result.SLORules.MetadataRecRules.Rules[i].Labels = removeLabels(result.SLORules.MetadataRecRules.Rules[i].Labels, preserveLabels) + } + + return nil +} + +func removeLabels(existingLabels map[string]string, preserveLabels map[string]bool) map[string]string { + newLabels := map[string]string{} + for k, v := range existingLabels { + if preserveLabels[k] { + newLabels[k] = v + } + } + return newLabels +} diff --git a/internal/plugin/slo/contrib/remove_labels_v1/plugin_test.go b/internal/plugin/slo/contrib/remove_labels_v1/plugin_test.go new file mode 100644 index 00000000..75b16f41 --- /dev/null +++ b/internal/plugin/slo/contrib/remove_labels_v1/plugin_test.go @@ -0,0 +1,415 @@ +package plugin_test + +import ( + "encoding/json" + "testing" + + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/stretchr/testify/assert" + + plugin "github.com/slok/sloth/internal/plugin/slo/contrib/remove_labels_v1" + "github.com/slok/sloth/pkg/common/model" + pluginslov1 "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1" + pluginslov1testing "github.com/slok/sloth/pkg/prometheus/plugin/slo/v1/testing" +) + +func MustJSONRawMessage(t *testing.T, v any) json.RawMessage { + j, err := json.Marshal(v) + if err != nil { + t.Fatalf("failed to marshal %T: %v", v, err) + } + return j +} + +func TestPlugin(t *testing.T) { + tests := map[string]struct { + config json.RawMessage + req pluginslov1.Request + res pluginslov1.Result + expRes pluginslov1.Result + expLoadErr bool + expErr bool + }{ + "Custom labels should be removed from all but sloth_slo_info.": { + config: MustJSONRawMessage(t, plugin.Config{}), + req: pluginslov1.Request{}, + res: pluginslov1.Result{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "5m", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + { + Record: "slo:sli_error:ratio_rate1h", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "1h", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:objective:ratio", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + { + Record: "sloth_slo_info", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + }}, + }, + }, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "5m", + }, + }, + { + Record: "slo:sli_error:ratio_rate1h", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "1h", + }, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:objective:ratio", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + }, + }, + { + Record: "sloth_slo_info", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + }}, + }, + }, + }, + "Preseve labels config should not be removed.": { + config: MustJSONRawMessage(t, plugin.Config{PreserveLabels: []string{"keep_this_label", "and_this_one_too"}}), + req: pluginslov1.Request{}, + res: pluginslov1.Result{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "5m", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + { + Record: "slo:sli_error:ratio_rate1h", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "1h", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:objective:ratio", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + { + Record: "sloth_slo_info", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + }}, + }, + }, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "5m", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + }, + }, + { + Record: "slo:sli_error:ratio_rate1h", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "1h", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + }, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:objective:ratio", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + }, + }, + { + Record: "sloth_slo_info", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + }}, + }, + }, + }, + "Don't remove labels from metrics in skipMetrics config.": { + config: MustJSONRawMessage(t, plugin.Config{ + PreserveLabels: []string{"keep_this_label", "and_this_one_too"}, + SkipMetrics: []string{"slo:objective:ratio"}, + }), + req: pluginslov1.Request{}, + res: pluginslov1.Result{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "5m", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + { + Record: "slo:sli_error:ratio_rate1h", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "1h", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:objective:ratio", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + { + Record: "sloth_slo_info", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + }}, + }, + }, + expRes: pluginslov1.Result{ + SLORules: model.PromSLORules{ + SLIErrorRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:sli_error:ratio_rate5m", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "5m", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + }, + }, + { + Record: "slo:sli_error:ratio_rate1h", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "sloth_window": "1h", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + }, + }, + }}, + MetadataRecRules: model.PromRuleGroup{Rules: []rulefmt.Rule{ + { + Record: "slo:objective:ratio", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + { + Record: "sloth_slo_info", + Labels: map[string]string{ + "sloth_service": "some_service", + "sloth_slo": "some_slo", + "sloth_id": "some_service_some_slo", + "keep_this_label": "some_value", + "and_this_one_too": "some_value", + "🦥_label_1": "🦥_1", + "🦥_label_2": "🦥_2", + }, + }, + }}, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + plugin, err := pluginslov1testing.NewTestPlugin(t.Context(), pluginslov1testing.TestPluginConfig{PluginConfiguration: test.config}) + if test.expLoadErr { + assert.Error(err) + return + } + assert.NoError(err) + + err = plugin.ProcessSLO(t.Context(), &test.req, &test.res) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, test.res) + } + }) + } +} + +func BenchmarkPluginYaegi(b *testing.B) { + plugin, err := pluginslov1testing.NewTestPlugin(b.Context(), pluginslov1testing.TestPluginConfig{ + PluginConfiguration: []byte(`{}`), + }) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkPluginGo(b *testing.B) { + plugin, err := plugin.NewPlugin([]byte(`{"labels":{"plugin_🦥_label_1":"🦥_1"}}`), pluginslov1.AppUtils{}) + if err != nil { + b.Fatal(err) + } + + for b.Loop() { + err = plugin.ProcessSLO(b.Context(), &pluginslov1.Request{}, &pluginslov1.Result{}) + if err != nil { + b.Fatal(err) + } + } +} From 42455d2fd94829d5637a24b213187c311d273c2e Mon Sep 17 00:00:00 2001 From: Adam Fox Date: Sat, 8 Nov 2025 19:26:08 +1030 Subject: [PATCH 115/173] Refactor removeLabels function in remove_labels/plugin.go --- .../plugin/slo/contrib/remove_labels_v1/plugin.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/plugin/slo/contrib/remove_labels_v1/plugin.go b/internal/plugin/slo/contrib/remove_labels_v1/plugin.go index 2eca84f4..ee5d7564 100644 --- a/internal/plugin/slo/contrib/remove_labels_v1/plugin.go +++ b/internal/plugin/slo/contrib/remove_labels_v1/plugin.go @@ -51,7 +51,7 @@ func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, re if skipMetrics[result.SLORules.SLIErrorRecRules.Rules[i].Record] { continue } - result.SLORules.SLIErrorRecRules.Rules[i].Labels = removeLabels(result.SLORules.SLIErrorRecRules.Rules[i].Labels, preserveLabels) + removeLabels(result.SLORules.SLIErrorRecRules.Rules[i].Labels, preserveLabels) } delete(preserveLabels, "sloth_window") @@ -59,18 +59,16 @@ func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, re if skipMetrics[result.SLORules.MetadataRecRules.Rules[i].Record] { continue } - result.SLORules.MetadataRecRules.Rules[i].Labels = removeLabels(result.SLORules.MetadataRecRules.Rules[i].Labels, preserveLabels) + removeLabels(result.SLORules.MetadataRecRules.Rules[i].Labels, preserveLabels) } return nil } -func removeLabels(existingLabels map[string]string, preserveLabels map[string]bool) map[string]string { - newLabels := map[string]string{} - for k, v := range existingLabels { - if preserveLabels[k] { - newLabels[k] = v +func removeLabels(labels map[string]string, preserveLabels map[string]bool) { + for k := range labels { + if !preserveLabels[k] { + delete(labels, k) } } - return newLabels } From c1e0397a4034abcb60a6d4df309511b5dfd62606 Mon Sep 17 00:00:00 2001 From: Adam Fox Date: Sat, 8 Nov 2025 19:31:28 +1030 Subject: [PATCH 116/173] Use struct{} instead of bool in skipMetrics and preserveLabels --- .../slo/contrib/remove_labels_v1/plugin.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/plugin/slo/contrib/remove_labels_v1/plugin.go b/internal/plugin/slo/contrib/remove_labels_v1/plugin.go index ee5d7564..485a8ddc 100644 --- a/internal/plugin/slo/contrib/remove_labels_v1/plugin.go +++ b/internal/plugin/slo/contrib/remove_labels_v1/plugin.go @@ -34,21 +34,21 @@ func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1. } func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { - preserveLabels := map[string]bool{"sloth_window": true} + preserveLabels := map[string]struct{}{"sloth_window": struct{}{}} for k := range conventions.GetSLOIDPromLabels(request.SLO) { - preserveLabels[k] = true + preserveLabels[k] = struct{}{} } for _, k := range p.config.PreserveLabels { - preserveLabels[k] = true + preserveLabels[k] = struct{}{} } - skipMetrics := map[string]bool{conventions.PromMetaSLOInfoMetric: true} + skipMetrics := map[string]struct{}{conventions.PromMetaSLOInfoMetric: struct{}{}} for _, k := range p.config.SkipMetrics { - skipMetrics[k] = true + skipMetrics[k] = struct{}{} } for i := range result.SLORules.SLIErrorRecRules.Rules { - if skipMetrics[result.SLORules.SLIErrorRecRules.Rules[i].Record] { + if _, ok := skipMetrics[result.SLORules.SLIErrorRecRules.Rules[i].Record]; ok { continue } removeLabels(result.SLORules.SLIErrorRecRules.Rules[i].Labels, preserveLabels) @@ -56,7 +56,7 @@ func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, re delete(preserveLabels, "sloth_window") for i := range result.SLORules.MetadataRecRules.Rules { - if skipMetrics[result.SLORules.MetadataRecRules.Rules[i].Record] { + if _, ok := skipMetrics[result.SLORules.MetadataRecRules.Rules[i].Record]; ok { continue } removeLabels(result.SLORules.MetadataRecRules.Rules[i].Labels, preserveLabels) @@ -65,9 +65,9 @@ func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, re return nil } -func removeLabels(labels map[string]string, preserveLabels map[string]bool) { +func removeLabels(labels map[string]string, preserveLabels map[string]struct{}) { for k := range labels { - if !preserveLabels[k] { + if _, ok := preserveLabels[k]; !ok { delete(labels, k) } } From 86a9f92b3ad465d201cb659da50c65aff55125c8 Mon Sep 17 00:00:00 2001 From: Adam Fox Date: Sat, 8 Nov 2025 19:55:31 +1030 Subject: [PATCH 117/173] Remove redundant struct{}'s --- internal/plugin/slo/contrib/remove_labels_v1/plugin.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/plugin/slo/contrib/remove_labels_v1/plugin.go b/internal/plugin/slo/contrib/remove_labels_v1/plugin.go index 485a8ddc..2aa9adac 100644 --- a/internal/plugin/slo/contrib/remove_labels_v1/plugin.go +++ b/internal/plugin/slo/contrib/remove_labels_v1/plugin.go @@ -34,7 +34,7 @@ func NewPlugin(configData json.RawMessage, _ pluginslov1.AppUtils) (pluginslov1. } func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, result *pluginslov1.Result) error { - preserveLabels := map[string]struct{}{"sloth_window": struct{}{}} + preserveLabels := map[string]struct{}{"sloth_window": {}} for k := range conventions.GetSLOIDPromLabels(request.SLO) { preserveLabels[k] = struct{}{} } @@ -42,7 +42,7 @@ func (p plugin) ProcessSLO(ctx context.Context, request *pluginslov1.Request, re preserveLabels[k] = struct{}{} } - skipMetrics := map[string]struct{}{conventions.PromMetaSLOInfoMetric: struct{}{}} + skipMetrics := map[string]struct{}{conventions.PromMetaSLOInfoMetric: {}} for _, k := range p.config.SkipMetrics { skipMetrics[k] = struct{}{} } From 4cf7878bf8d90ff65e91966310bcc11330ef7871 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 07:01:36 +0000 Subject: [PATCH 118/173] build(deps): bump golang from 1.25 to 1.25.4 in /docker/dev Bumps golang from 1.25 to 1.25.4. --- updated-dependencies: - dependency-name: golang dependency-version: 1.25.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docker/dev/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index aa820052..851bc70a 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25 +FROM golang:1.25.4 LABEL org.opencontainers.image.source=https://github.com/slok/sloth From 94780a7e9538932e47c402aebc5eb4ff7e3c7c99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 07:01:38 +0000 Subject: [PATCH 119/173] build(deps): bump actions/checkout from 5 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 16 ++++++++-------- .github/workflows/generate.yaml | 2 +- .github/workflows/helmrelease.yaml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 97bf8268..d82432c1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,7 @@ jobs: # Execute the checks inside the container instead the VM. container: golangci/golangci-lint:v2.4.0-alpine steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - run: | # We need this go flag because it started to error after golangci-lint is using Go 1.21. # TODO(slok): Remove it on next (>1.54.1) golangci-lint upgrade to check if this problem has gone. @@ -20,7 +20,7 @@ jobs: name: Unit test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -35,7 +35,7 @@ jobs: name: Helm chart test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -52,7 +52,7 @@ jobs: name: Integration test CLI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -74,7 +74,7 @@ jobs: matrix: kubernetes: [1.31.12, 1.32.8, 1.33.4, 1.34.0] steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -124,7 +124,7 @@ jobs: name: Release images runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Docker login run: docker login ghcr.io -u ${{ github.actor }} -p "${{ secrets.GITHUB_TOKEN }}" - name: Build and publish docker images @@ -147,7 +147,7 @@ jobs: runs-on: ubuntu-latest steps: - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} # Sets VERSION env var. - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Docker login run: docker login ghcr.io -u ${{ github.actor }} -p "${{ secrets.GITHUB_TOKEN }}" - name: Build and publish docker images @@ -168,7 +168,7 @@ jobs: runs-on: ubuntu-latest steps: - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} # Sets VERSION env var. - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Build binaries run: | mkdir -p ./bin diff --git a/.github/workflows/generate.yaml b/.github/workflows/generate.yaml index b44ff844..dc2a193a 100644 --- a/.github/workflows/generate.yaml +++ b/.github/workflows/generate.yaml @@ -11,7 +11,7 @@ jobs: name: Generate the SLOs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: download and setup generator binary run: | wget https://github.com/slok/sloth/releases/download/v0.9.0/sloth-linux-amd64 diff --git a/.github/workflows/helmrelease.yaml b/.github/workflows/helmrelease.yaml index e3437ea8..84238adb 100644 --- a/.github/workflows/helmrelease.yaml +++ b/.github/workflows/helmrelease.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 From b6df02b75b0e07ea54932dcccc4c30666a4da5c7 Mon Sep 17 00:00:00 2001 From: Xabier Larrakoetxea Date: Sun, 2 Nov 2025 11:26:04 +0100 Subject: [PATCH 120/173] Bootstrap the first iteration of the new Sloth UI Signed-off-by: Xabier Larrakoetxea --- .mockery.yml | 3 + CHANGELOG.md | 4 + cmd/sloth/commands/server.go | 236 +++ cmd/sloth/main.go | 2 + go.mod | 3 + go.sum | 28 + internal/http/backend/app/app.go | 46 + internal/http/backend/app/pagination_utils.go | 92 + internal/http/backend/app/service.go | 61 + internal/http/backend/app/service_test.go | 127 ++ internal/http/backend/app/slo.go | 277 +++ internal/http/backend/app/slo_test.go | 777 ++++++++ internal/http/backend/app/time_utils.go | 123 ++ internal/http/backend/model/model.go | 37 + internal/http/backend/storage/fake/fake.go | 394 ++++ .../http/backend/storage/prometheus/cache.go | 367 ++++ .../backend/storage/prometheus/prometheus.go | 265 +++ .../storage/prometheus/prometheus_test.go | 784 ++++++++ .../prometheus/prometheusmock/mocks.go | 1587 +++++++++++++++++ .../http/backend/storage/search/search.go | 139 ++ .../backend/storage/search/search_test.go | 207 +++ internal/http/backend/storage/storage.go | 35 + .../http/backend/storage/storagemock/mocks.go | 704 ++++++++ internal/http/ui/common_test.go | 53 + internal/http/ui/common_tpl.go | 74 + internal/http/ui/common_uplot.go | 63 + internal/http/ui/common_urls.go | 89 + internal/http/ui/handler_index.go | 11 + internal/http/ui/handler_index_test.go | 51 + internal/http/ui/handler_select_service.go | 162 ++ .../http/ui/handler_select_service_test.go | 249 +++ internal/http/ui/handler_select_slo.go | 153 ++ internal/http/ui/handler_select_slo_test.go | 275 +++ internal/http/ui/handler_service_details.go | 129 ++ .../http/ui/handler_service_details_test.go | 222 +++ internal/http/ui/handler_slo_details.go | 320 ++++ internal/http/ui/handler_slo_details_test.go | 284 +++ internal/http/ui/htmx/htmx.go | 141 ++ internal/http/ui/metrics.go | 16 + internal/http/ui/midleware.go | 28 + internal/http/ui/routes.go | 37 + internal/http/ui/static/css/main.css | 31 + .../static/img/favicon/apple-touch-icon.png | Bin 0 -> 11407 bytes .../ui/static/img/favicon/favicon-32x32.png | Bin 0 -> 1382 bytes internal/http/ui/static/js/main.js | 162 ++ .../templates/app/service/comp_slo_list.tmpl | 67 + .../http/ui/templates/app/service/page.tmpl | 15 + .../app/services/comp_service_list.tmpl | 62 + .../http/ui/templates/app/services/page.tmpl | 30 + .../templates/app/slo/comp_budget_chart.tmpl | 33 + .../ui/templates/app/slo/comp_sli_chart.tmpl | 37 + .../ui/templates/app/slo/comp_slo_data.tmpl | 7 + .../http/ui/templates/app/slo/comp_stats.tmpl | 29 + internal/http/ui/templates/app/slo/page.tmpl | 16 + .../ui/templates/app/slos/comp_slo_list.tmpl | 69 + internal/http/ui/templates/app/slos/page.tmpl | 30 + internal/http/ui/templates/shared/footer.tmpl | 3 + internal/http/ui/templates/shared/head.tmpl | 24 + internal/http/ui/templates/shared/nav.tmpl | 16 + internal/http/ui/templates/slo/list-all.tmpl | 10 + internal/http/ui/tpl.go | 143 ++ internal/http/ui/ui.go | 107 ++ internal/http/ui/uimock/mocks.go | 379 ++++ ...b_com-slok-sloth-pkg-common-conventions.go | 2 + ...-slok-sloth-pkg-common-utils-prometheus.go | 1 + ...b_com-slok-sloth-pkg-common-conventions.go | 2 + ...-slok-sloth-pkg-common-utils-prometheus.go | 1 + pkg/common/conventions/conventions.go | 3 +- pkg/common/conventions/slo.go | 3 +- pkg/common/utils/prometheus/prometheus.go | 10 + 70 files changed, 9945 insertions(+), 2 deletions(-) create mode 100644 cmd/sloth/commands/server.go create mode 100644 internal/http/backend/app/app.go create mode 100644 internal/http/backend/app/pagination_utils.go create mode 100644 internal/http/backend/app/service.go create mode 100644 internal/http/backend/app/service_test.go create mode 100644 internal/http/backend/app/slo.go create mode 100644 internal/http/backend/app/slo_test.go create mode 100644 internal/http/backend/app/time_utils.go create mode 100644 internal/http/backend/model/model.go create mode 100644 internal/http/backend/storage/fake/fake.go create mode 100644 internal/http/backend/storage/prometheus/cache.go create mode 100644 internal/http/backend/storage/prometheus/prometheus.go create mode 100644 internal/http/backend/storage/prometheus/prometheus_test.go create mode 100644 internal/http/backend/storage/prometheus/prometheusmock/mocks.go create mode 100644 internal/http/backend/storage/search/search.go create mode 100644 internal/http/backend/storage/search/search_test.go create mode 100644 internal/http/backend/storage/storage.go create mode 100644 internal/http/backend/storage/storagemock/mocks.go create mode 100644 internal/http/ui/common_test.go create mode 100644 internal/http/ui/common_tpl.go create mode 100644 internal/http/ui/common_uplot.go create mode 100644 internal/http/ui/common_urls.go create mode 100644 internal/http/ui/handler_index.go create mode 100644 internal/http/ui/handler_index_test.go create mode 100644 internal/http/ui/handler_select_service.go create mode 100644 internal/http/ui/handler_select_service_test.go create mode 100644 internal/http/ui/handler_select_slo.go create mode 100644 internal/http/ui/handler_select_slo_test.go create mode 100644 internal/http/ui/handler_service_details.go create mode 100644 internal/http/ui/handler_service_details_test.go create mode 100644 internal/http/ui/handler_slo_details.go create mode 100644 internal/http/ui/handler_slo_details_test.go create mode 100644 internal/http/ui/htmx/htmx.go create mode 100644 internal/http/ui/metrics.go create mode 100644 internal/http/ui/midleware.go create mode 100644 internal/http/ui/routes.go create mode 100644 internal/http/ui/static/css/main.css create mode 100644 internal/http/ui/static/img/favicon/apple-touch-icon.png create mode 100644 internal/http/ui/static/img/favicon/favicon-32x32.png create mode 100644 internal/http/ui/static/js/main.js create mode 100644 internal/http/ui/templates/app/service/comp_slo_list.tmpl create mode 100644 internal/http/ui/templates/app/service/page.tmpl create mode 100644 internal/http/ui/templates/app/services/comp_service_list.tmpl create mode 100644 internal/http/ui/templates/app/services/page.tmpl create mode 100644 internal/http/ui/templates/app/slo/comp_budget_chart.tmpl create mode 100644 internal/http/ui/templates/app/slo/comp_sli_chart.tmpl create mode 100644 internal/http/ui/templates/app/slo/comp_slo_data.tmpl create mode 100644 internal/http/ui/templates/app/slo/comp_stats.tmpl create mode 100644 internal/http/ui/templates/app/slo/page.tmpl create mode 100644 internal/http/ui/templates/app/slos/comp_slo_list.tmpl create mode 100644 internal/http/ui/templates/app/slos/page.tmpl create mode 100644 internal/http/ui/templates/shared/footer.tmpl create mode 100644 internal/http/ui/templates/shared/head.tmpl create mode 100644 internal/http/ui/templates/shared/nav.tmpl create mode 100644 internal/http/ui/templates/slo/list-all.tmpl create mode 100644 internal/http/ui/tpl.go create mode 100644 internal/http/ui/ui.go create mode 100644 internal/http/ui/uimock/mocks.go diff --git a/.mockery.yml b/.mockery.yml index 7d97c64d..cb8260db 100644 --- a/.mockery.yml +++ b/.mockery.yml @@ -7,3 +7,6 @@ template: testify packages: github.com/slok/sloth/internal/app/generate: {interfaces: {SLOPluginGetter}} github.com/slok/sloth/internal/storage/fs: {interfaces: {SLIPluginLoader,SLOPluginLoader, K8sTransformPluginLoader}} + github.com/slok/sloth/internal/http/backend/storage: {interfaces: {SLOGetter, ServiceGetter}} + github.com/slok/sloth/internal/http/backend/storage/prometheus: {interfaces: {PrometheusAPIClient}} + github.com/slok/sloth/internal/http/ui: {interfaces: {ServiceApp}} diff --git a/CHANGELOG.md b/CHANGELOG.md index 64fb5669..699a02ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - `sloth.dev/k8stransform/prom-operator-prometheus-rule/v1` K8s transformer plugin. - Users can now create multiple K8s objects as the output of the SLO generated rules. - Sloth lib support for K8s transformer plugins using `WriteResultAsK8sObjects`. +- New `server` command that serves the new UI. +- UI: Service listing and searching. +- UI: SLO listing, searching and filtered by service. +- UI: SLO details with stats, alerts state, SLI chart and budged burn in period chart. ### Changed diff --git a/cmd/sloth/commands/server.go b/cmd/sloth/commands/server.go new file mode 100644 index 00000000..0a854dc0 --- /dev/null +++ b/cmd/sloth/commands/server.go @@ -0,0 +1,236 @@ +package commands + +import ( + "context" + "fmt" + "net/http" + "net/http/pprof" + "os/signal" + "syscall" + "time" + + "github.com/alecthomas/kingpin/v2" + "github.com/oklog/run" + promapi "github.com/prometheus/client_golang/api" + promv1 "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/prometheus/client_golang/prometheus/promhttp" + + backendapp "github.com/slok/sloth/internal/http/backend/app" + "github.com/slok/sloth/internal/http/backend/storage" + storagefake "github.com/slok/sloth/internal/http/backend/storage/fake" + storageprometheus "github.com/slok/sloth/internal/http/backend/storage/prometheus" + storagesearch "github.com/slok/sloth/internal/http/backend/storage/search" + "github.com/slok/sloth/internal/http/ui" + "github.com/slok/sloth/internal/log" +) + +type serverCommand struct { + statusServer struct { + address string + healthCheckPath string + metricsPath string + pprofPath string + } + appServer struct { + address string + } + + prometheus struct { + fake bool + promAddress string + } +} + +// NewServerCommand returns the UI command. +func NewServerCommand(app *kingpin.Application) Command { + c := &serverCommand{} + cmd := app.Command("server", "Starts the Sloth web server.") + cmd.Flag("app-listen-address", "Application listen address.").Default(":8080").StringVar(&c.appServer.address) + cmd.Flag("status-listen-address", "Status (health check, metrics, pprof...) listen address.").Default(":8081").StringVar(&c.statusServer.address) + cmd.Flag("health-check-path", "Health check path.").Default("/status").StringVar(&c.statusServer.healthCheckPath) + cmd.Flag("metrics-path", "Prometheus metrics path where metrics will be served.").Default("/metrics").StringVar(&c.statusServer.metricsPath) + cmd.Flag("pprof-path", "PProf path where debug tool is available.").Default("/debug/pprof").StringVar(&c.statusServer.pprofPath) + + cmd.Flag("fake-prometheus", "Enable fake Prometheus server.").BoolVar(&c.prometheus.fake) + cmd.Flag("prometheus-address", "Prometheus server address.").Default("http://localhost:9090").StringVar(&c.prometheus.promAddress) + + return c +} + +func (c serverCommand) Name() string { return "server" } +func (c serverCommand) Run(ctx context.Context, config RootConfig) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + logger := config.Logger.WithValues(log.Kv{"command": c.Name()}) + + // Prepare vault refresh + var g run.Group + + // Handle cancellation. + { + // Listen for shutdown signals, when signal received, stop main context to start the graceful shutdown. + ctx, signalCancel := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT) + defer signalCancel() + + exitC := make(chan struct{}) + + g.Add( + func() error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-exitC: + } + + return nil + }, + func(_ error) { + close(exitC) + }, + ) + } + + // Status and metadata server (health checks, metrics...). + { + logger := logger.WithValues(log.Kv{ + "addr": c.statusServer.address, + "metrics": c.statusServer.metricsPath, + "health-check": c.statusServer.healthCheckPath, + "pprof": c.statusServer.pprofPath, + }) + mux := http.NewServeMux() + + // Pprof. + mux.HandleFunc(c.statusServer.pprofPath+"/", pprof.Index) + mux.HandleFunc(c.statusServer.pprofPath+"/cmdline", pprof.Cmdline) + mux.HandleFunc(c.statusServer.pprofPath+"/profile", pprof.Profile) + mux.HandleFunc(c.statusServer.pprofPath+"/symbol", pprof.Symbol) + mux.HandleFunc(c.statusServer.pprofPath+"/trace", pprof.Trace) + + // Metrics. + mux.Handle(c.statusServer.metricsPath, promhttp.Handler()) + + // Health checks. + mux.HandleFunc(c.statusServer.healthCheckPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("ok")) })) + + server := http.Server{ + Addr: c.statusServer.address, + Handler: mux, + } + + g.Add( + func() error { + logger.Infof("HTTP server listening...") + return server.ListenAndServe() + }, + func(_ error) { + logger.Infof("Start draining connections") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := server.Shutdown(ctx) + if err != nil { + logger.Errorf("error while shutting down the server: %s", err) + } else { + logger.Infof("Server stopped") + } + }, + ) + } + + // Application server. + { + var repo unifiedRepository + + switch { + case c.prometheus.fake: + logger.Warningf("Using fake Prometheus storage backend") + repo = storagefake.NewFakeRepository() + case c.prometheus.promAddress != "": + logger.Infof("Using Prometheus storage backend at %s", c.prometheus.promAddress) + client, err := promapi.NewClient(promapi.Config{ + Address: c.prometheus.promAddress, + }) + if err != nil { + return fmt.Errorf("could not create prometheus api client: %w", err) + } + + repo, err = storageprometheus.NewRepository(ctx, storageprometheus.RepositoryConfig{ + PrometheusClient: promv1.NewAPI(client), + Logger: logger, + }) + if err != nil { + return fmt.Errorf("could not create prometheus storage repository: %w", err) + } + default: + return fmt.Errorf("no storage backend configured") + } + + // Wrap repo with search capabilities. + repo, err := storagesearch.NewSearchRepositoryWrapper(repo, repo) + if err != nil { + return fmt.Errorf("could not create search repository wrapper: %w", err) + } + + app, err := backendapp.NewApp(backendapp.AppConfig{ + ServiceGetter: repo, + SLOGetter: repo, + }) + if err != nil { + return fmt.Errorf("could not create app: %w", err) + } + + // Web UI. + uiHandler, err := ui.NewUI(ui.UIConfig{ + Logger: logger, + ServiceApp: app, + }) + if err != nil { + return fmt.Errorf("could not create ui handler: %w", err) + } + + mux := http.NewServeMux() + mux.Handle(ui.ServePrefix+"/", uiHandler) + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, ui.ServePrefix, http.StatusSeeOther) + })) // Root redirect to UI. + + server := http.Server{ + Addr: c.appServer.address, + Handler: mux, + } + + logger = logger.WithValues(log.Kv{"addr": c.appServer.address}) + g.Add( + func() error { + logger.Infof("HTTP server listening...") + return server.ListenAndServe() + }, + func(_ error) { + logger.Infof("Start draining connections") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err := server.Shutdown(ctx) + if err != nil { + logger.Errorf("error while shutting down the server: %s", err) + } else { + logger.Infof("Server stopped") + } + }, + ) + } + + err := g.Run() + if err != nil { + return err + } + + return nil +} + +type unifiedRepository interface { + storage.SLOGetter + storage.ServiceGetter +} diff --git a/cmd/sloth/main.go b/cmd/sloth/main.go index bc397d33..55645014 100644 --- a/cmd/sloth/main.go +++ b/cmd/sloth/main.go @@ -24,12 +24,14 @@ func Run(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io. // Setup commands (registers flags). generateCmd := commands.NewGenerateCommand(app) kubeCtrlCmd := commands.NewKubeControllerCommand(app) + serverCmd := commands.NewServerCommand(app) validateCmd := commands.NewValidateCommand(app) versionCmd := commands.NewVersionCommand(app) cmds := map[string]commands.Command{ generateCmd.Name(): generateCmd, kubeCtrlCmd.Name(): kubeCtrlCmd, + serverCmd.Name(): serverCmd, validateCmd.Name(): validateCmd, versionCmd.Name(): versionCmd, } diff --git a/go.mod b/go.mod index 9c30e6a1..ca4fe92b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/VictoriaMetrics/metricsql v0.84.8 github.com/alecthomas/kingpin/v2 v2.4.0 github.com/caarlos0/env/v11 v11.3.1 + github.com/go-chi/chi/v5 v5.2.3 + github.com/lithammer/fuzzysearch v1.1.8 github.com/oklog/run v1.2.0 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.86.1 github.com/prometheus-operator/prometheus-operator/pkg/client v0.86.1 @@ -14,6 +16,7 @@ require ( github.com/prometheus/common v0.67.2 github.com/prometheus/prometheus v0.307.3 github.com/sirupsen/logrus v1.9.3 + github.com/slok/go-http-metrics v0.13.0 github.com/slok/reload v0.2.0 github.com/spotahome/kooper/v2 v2.9.0 github.com/stretchr/testify v1.11.1 diff --git a/go.sum b/go.sum index 0a3efff2..fc415bba 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -146,6 +148,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= +github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= 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= @@ -192,6 +196,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slok/go-http-metrics v0.13.0 h1:lQDyJJx9wKhmbliyUsZ2l6peGnXRHjsjoqPt5VYzcP8= +github.com/slok/go-http-metrics v0.13.0/go.mod h1:HIr7t/HbN2sJaunvnt9wKP9xoBBVZFo1/KiHU3b0w+4= github.com/slok/reload v0.2.0 h1:8ezO7EsaYMUCX2g1ZAdHTxD0Mo9oDn2legZZBoFMUEI= github.com/slok/reload v0.2.0/go.mod h1:vyJiGsSZTUx08vDoEiVszBp8Jr+bsMwIK/U1pK7XTmI= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= @@ -224,6 +230,7 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8 github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= @@ -245,16 +252,22 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90= golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= 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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= @@ -262,18 +275,31 @@ golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= @@ -282,6 +308,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm 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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/http/backend/app/app.go b/internal/http/backend/app/app.go new file mode 100644 index 00000000..3c22147c --- /dev/null +++ b/internal/http/backend/app/app.go @@ -0,0 +1,46 @@ +package app + +import ( + "fmt" + "time" + + "github.com/slok/sloth/internal/http/backend/storage" +) + +type AppConfig struct { + ServiceGetter storage.ServiceGetter + SLOGetter storage.SLOGetter + TimeNowFunc func() time.Time +} + +func (c *AppConfig) defaults() error { + if c.ServiceGetter == nil { + return fmt.Errorf("service getter is required") + } + if c.SLOGetter == nil { + return fmt.Errorf("slo getter is required") + } + if c.TimeNowFunc == nil { + c.TimeNowFunc = time.Now + } + + return nil +} + +type App struct { + serviceGetter storage.ServiceGetter + sloGetter storage.SLOGetter + timeNowFunc func() time.Time +} + +func NewApp(config AppConfig) (*App, error) { + if err := config.defaults(); err != nil { + return nil, err + } + + return &App{ + serviceGetter: config.ServiceGetter, + sloGetter: config.SLOGetter, + timeNowFunc: config.TimeNowFunc, + }, nil +} diff --git a/internal/http/backend/app/pagination_utils.go b/internal/http/backend/app/pagination_utils.go new file mode 100644 index 00000000..021aa261 --- /dev/null +++ b/internal/http/backend/app/pagination_utils.go @@ -0,0 +1,92 @@ +package app + +import ( + "encoding/base64" + "encoding/json" +) + +// paginationCursor is the cursor used for pagination. +type paginationCursor struct { + Size int `json:"size"` + Page int `json:"page"` +} + +// PaginationCursor are the pagination information used for cursor based pagination. +type PaginationCursors struct { + PrevCursor string + NextCursor string + HasNext bool + HasPrevious bool +} + +func (c *paginationCursor) defaults() { + if c.Size <= 0 { + c.Size = 30 + } + + if c.Size > 100 { + c.Size = 100 + } + + if c.Page <= 0 { + c.Page = 1 + } +} + +func paginationCursorToString(c paginationCursor) string { + data, _ := json.Marshal(c) + base64Data := base64.StdEncoding.EncodeToString(data) + return base64Data +} + +func paginationCursorFromString(encoded string) (*paginationCursor, error) { + data, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, err + } + var c paginationCursor + err = json.Unmarshal(data, &c) + if err != nil { + return nil, err + } + return &c, nil +} + +func paginateSlice[T any](items []T, cursorS string) (paginatedItems []T, cursors PaginationCursors) { + cursor, err := paginationCursorFromString(cursorS) + if err != nil { + cursor = &paginationCursor{} + } + cursor.defaults() + + startIndex := (cursor.Page - 1) * cursor.Size + if startIndex > len(items) { + startIndex = len(items) + } + endIndex := startIndex + cursor.Size + if endIndex > len(items) { + endIndex = len(items) + } + paginatedItems = items[startIndex:endIndex] + nextCursor := "" + if endIndex < len(items) { + nextCursor = paginationCursorToString(paginationCursor{ + Size: cursor.Size, + Page: cursor.Page + 1, + }) + } + prevCursor := "" + if cursor.Page > 1 { + prevCursor = paginationCursorToString(paginationCursor{ + Size: cursor.Size, + Page: cursor.Page - 1, + }) + } + + return paginatedItems, PaginationCursors{ + PrevCursor: prevCursor, + NextCursor: nextCursor, + HasNext: nextCursor != "", + HasPrevious: prevCursor != "", + } +} diff --git a/internal/http/backend/app/service.go b/internal/http/backend/app/service.go new file mode 100644 index 00000000..8afb9ab7 --- /dev/null +++ b/internal/http/backend/app/service.go @@ -0,0 +1,61 @@ +package app + +import ( + "context" + "slices" + "strings" + + "github.com/slok/sloth/internal/http/backend/model" + "github.com/slok/sloth/internal/http/backend/storage" +) + +type ListServicesRequest struct { + FilterSearchInput string + Cursor string +} + +type ListServicesResponse struct { + Services []ServiceAlerts + PaginationCursors PaginationCursors +} + +func (a *App) ListServices(ctx context.Context, req ListServicesRequest) (*ListServicesResponse, error) { + var err error + var services []storage.ServiceAndAlerts + + if req.FilterSearchInput != "" { + services, err = a.serviceGetter.ListServiceAndAlertsByServiceSearch(ctx, req.FilterSearchInput) + if err != nil { + return nil, err + } + } else { + services, err = a.serviceGetter.ListAllServiceAndAlerts(ctx) + if err != nil { + return nil, err + } + } + + svcs := []ServiceAlerts{} + for _, sa := range services { + svcs = append(svcs, ServiceAlerts{ + Service: sa.Service, + Alerts: sa.Alerts, + }) + } + + slices.SortStableFunc(svcs, func(x, y ServiceAlerts) int { + return strings.Compare(x.Service.ID, y.Service.ID) + }) + + // Handle pagination here for now, storage returns all. + psvcs, cursors := paginateSlice(svcs, req.Cursor) + return &ListServicesResponse{ + Services: psvcs, + PaginationCursors: cursors, + }, nil +} + +type ServiceAlerts struct { + Service model.Service + Alerts []model.SLOAlerts +} diff --git a/internal/http/backend/app/service_test.go b/internal/http/backend/app/service_test.go new file mode 100644 index 00000000..fc05793d --- /dev/null +++ b/internal/http/backend/app/service_test.go @@ -0,0 +1,127 @@ +package app_test + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/slok/sloth/internal/http/backend/app" + "github.com/slok/sloth/internal/http/backend/model" + "github.com/slok/sloth/internal/http/backend/storage" + "github.com/slok/sloth/internal/http/backend/storage/storagemock" +) + +func TestListServices(t *testing.T) { + tests := map[string]struct { + mock func(m *storagemock.ServiceGetter) + req app.ListServicesRequest + expResp func() *app.ListServicesResponse + expErr error + }{ + "Getting services successfully should return them properly.": { + mock: func(m *storagemock.ServiceGetter) { + m.On("ListAllServiceAndAlerts", mock.Anything).Return([]storage.ServiceAndAlerts{ + { + Service: model.Service{ID: "svc-2"}, + Alerts: []model.SLOAlerts{ + { + FiringWarning: &model.Alert{Name: "warn-2"}, + }, + }, + }, + { + Service: model.Service{ID: "svc-1"}, + Alerts: []model.SLOAlerts{ + { + FiringWarning: &model.Alert{Name: "warn-1"}, + FiringPage: &model.Alert{Name: "page-1"}, + }, + }, + }, + }, nil) + }, + expResp: func() *app.ListServicesResponse { + return &app.ListServicesResponse{ + Services: []app.ServiceAlerts{ + { + Service: model.Service{ID: "svc-1"}, + Alerts: []model.SLOAlerts{ + { + FiringWarning: &model.Alert{Name: "warn-1"}, + FiringPage: &model.Alert{Name: "page-1"}, + }, + }, + }, + { + Service: model.Service{ID: "svc-2"}, + Alerts: []model.SLOAlerts{ + { + FiringWarning: &model.Alert{Name: "warn-2"}, + }, + }, + }, + }, + } + }, + }, + + "Getting services paginated should return them properly.": { + req: app.ListServicesRequest{ + Cursor: "eyJzaXplIjozMCwicGFnZSI6M30=", + }, + mock: func(m *storagemock.ServiceGetter) { + // Returns all. + svcs := []storage.ServiceAndAlerts{} + for i := 1; i <= 200; i++ { + svcs = append(svcs, storage.ServiceAndAlerts{ + Service: model.Service{ID: fmt.Sprintf("svc-%03d", i)}, + }) + } + m.On("ListAllServiceAndAlerts", mock.Anything).Return(svcs, nil) + }, + expResp: func() *app.ListServicesResponse { + svcs := []app.ServiceAlerts{} + for i := 61; i <= 90; i++ { + svcs = append(svcs, app.ServiceAlerts{ + Service: model.Service{ID: fmt.Sprintf("svc-%03d", i)}, + }) + } + return &app.ListServicesResponse{ + Services: svcs, + PaginationCursors: app.PaginationCursors{ + PrevCursor: "eyJzaXplIjozMCwicGFnZSI6Mn0=", + NextCursor: "eyJzaXplIjozMCwicGFnZSI6NH0=", + HasNext: true, + HasPrevious: true, + }, + } + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + mServiceGetter := storagemock.NewServiceGetter(t) + test.mock(mServiceGetter) + + a, err := app.NewApp(app.AppConfig{ + ServiceGetter: mServiceGetter, + SLOGetter: storagemock.NewSLOGetter(t), + }) + require.NoError(t, err) + resp, err := a.ListServices(context.TODO(), test.req) + + if test.expErr != nil { + assert.Error(err) + + } else if assert.NoError(err) { + assert.Equal(test.expResp(), resp) + } + }) + } +} diff --git a/internal/http/backend/app/slo.go b/internal/http/backend/app/slo.go new file mode 100644 index 00000000..a1208c10 --- /dev/null +++ b/internal/http/backend/app/slo.go @@ -0,0 +1,277 @@ +package app + +import ( + "context" + "fmt" + "slices" + "strings" + "time" + + "github.com/slok/sloth/internal/http/backend/model" + "github.com/slok/sloth/internal/http/backend/storage" +) + +type ListSLOsRequest struct { + FilterServiceID string // Used for filtering SLOs by service ID. + FilterSearchInput string // Used for searching SLOs by name. + Cursor string +} + +func (r *ListSLOsRequest) defaults() error { + return nil +} + +type ListSLOsResponse struct { + SLOs []RealTimeSLODetails + PaginationCursors PaginationCursors +} + +func (a *App) ListSLOs(ctx context.Context, req ListSLOsRequest) (*ListSLOsResponse, error) { + err := req.defaults() + if err != nil { + return nil, err + } + + // Check if we need to filter by service or not. + var slos []storage.SLOInstantDetails + switch { + // Return all specific service SLOs. + case req.FilterSearchInput == "" && req.FilterServiceID != "": + slos, err = a.sloGetter.ListSLOInstantDetailsService(ctx, req.FilterServiceID) + if err != nil { + return nil, fmt.Errorf("could not list service SLOs: %w", err) + } + + // Search on all specific service SLOs. + case req.FilterSearchInput != "" && req.FilterServiceID != "": + slos, err = a.sloGetter.ListSLOInstantDetailsServiceBySLOSearch(ctx, req.FilterServiceID, req.FilterSearchInput) + if err != nil { + return nil, fmt.Errorf("could not list service SLOs: %w", err) + } + + // Search on all SLOs. + case req.FilterSearchInput != "" && req.FilterServiceID == "": + slos, err = a.sloGetter.ListSLOInstantDetailsBySLOSearch(ctx, req.FilterSearchInput) + if err != nil { + return nil, fmt.Errorf("could not list SLOs: %w", err) + } + // Return all. + default: + slos, err = a.sloGetter.ListSLOInstantDetails(ctx) + if err != nil { + return nil, fmt.Errorf("could not list SLOs: %w", err) + } + } + + rtSLOs := make([]RealTimeSLODetails, 0, len(slos)) + for _, s := range slos { + rtSLOs = append(rtSLOs, RealTimeSLODetails{ + SLO: s.SLO, + Alerts: s.Alerts, + Budget: s.BudgetDetails, + }) + } + + slices.SortStableFunc(rtSLOs, func(x, y RealTimeSLODetails) int { + return strings.Compare(x.SLO.ID, y.SLO.ID) + }) + + prtSLOs, cursors := paginateSlice(rtSLOs, req.Cursor) + + return &ListSLOsResponse{ + SLOs: prtSLOs, + PaginationCursors: cursors, + }, nil +} + +type RealTimeSLODetails struct { + SLO model.SLO + Alerts model.SLOAlerts + Budget model.SLOBudgetDetails +} + +type GetSLORequest struct { + SLOID string +} + +func (r *GetSLORequest) defaults() error { + if r.SLOID == "" { + return fmt.Errorf("SLO ID is required") + } + return nil +} + +type GetSLOResponse struct { + SLO RealTimeSLODetails +} + +func (a *App) GetSLO(ctx context.Context, req GetSLORequest) (*GetSLOResponse, error) { + err := req.defaults() + if err != nil { + return nil, err + } + + sloDetails, err := a.sloGetter.GetSLOInstantDetails(ctx, req.SLOID) + if err != nil { + return nil, err + } + + return &GetSLOResponse{ + SLO: RealTimeSLODetails{ + SLO: sloDetails.SLO, + Alerts: sloDetails.Alerts, + Budget: sloDetails.BudgetDetails, + }, + }, nil +} + +type ListSLIAvailabilityRangeRequest struct { + SLOID string + From time.Time + To time.Time // If missing, now is used. +} + +func (r *ListSLIAvailabilityRangeRequest) defaults() error { + if r.SLOID == "" { + return fmt.Errorf("SLO ID is required") + } + if r.From.IsZero() { + return fmt.Errorf("from time is required") + } + r.From = r.From.UTC() + + if r.To.IsZero() { + r.To = time.Now().UTC() + } + + if r.To.Before(r.From) { + return fmt.Errorf("to time must be after from time") + } + + if r.To.Sub(r.From) < (30 * time.Minute) { + return fmt.Errorf("time range must be at least 30 minutes") + } + + return nil +} + +type ListSLIAvailabilityRangeResponse struct { + AvailabilityDataPoints []model.DataPoint +} + +func (a *App) ListSLIAvailabilityRange(ctx context.Context, req ListSLIAvailabilityRangeRequest) (*ListSLIAvailabilityRangeResponse, error) { + err := req.defaults() + if err != nil { + return nil, err + } + + // Calculate steps based on time range. + step := calculateStepsForTimeRange(req.From, req.To) + + // Get data points. + dataPoints, err := a.sloGetter.GetSLIAvailabilityInRange(ctx, req.SLOID, req.From, req.To, step) + if err != nil { + return nil, err + } + + // Sanitize data points in case there are empty gaps. + dataPoints = sanitizeDataPoints(dataPoints, req.From, req.To, step) + + return &ListSLIAvailabilityRangeResponse{ + AvailabilityDataPoints: dataPoints, + }, nil +} + +type BudgetRangeType string + +const ( + BudgetRangeTypeYearly BudgetRangeType = "yearly" // 365 days. + BudgetRangeTypeQuarterly BudgetRangeType = "quarterly" // 90 days. + BudgetRangeTypeMonthly BudgetRangeType = "monthly" // 30 days. + BudgetRangeTypeWeekly BudgetRangeType = "weekly" // 7 days. +) + +type ListBurnedBudgetRangeRequest struct { + SLOID string + BudgetRangeType +} + +func (r *ListBurnedBudgetRangeRequest) defaults() error { + if r.SLOID == "" { + return fmt.Errorf("SLO ID is required") + } + + if r.BudgetRangeType == "" { + r.BudgetRangeType = BudgetRangeTypeMonthly + } + + return nil +} + +type ListBurnedBudgetRangeResponse struct { + RealBurnedDataPoints []model.DataPoint + PerfectBurnedDataPoints []model.DataPoint +} + +func (a *App) ListBurnedBudgetRange(ctx context.Context, req ListBurnedBudgetRangeRequest) (*ListBurnedBudgetRangeResponse, error) { + err := req.defaults() + if err != nil { + return nil, err + } + + // Get SLO details to calculate from and to. + sloDetails, err := a.sloGetter.GetSLOInstantDetails(ctx, req.SLOID) + if err != nil { + return nil, fmt.Errorf("could not get SLO details: %w", err) + } + + // Based on today's date, calculate from and to. + to := a.timeNowFunc().UTC() + from, err := startOfPeriod(to, req.BudgetRangeType) + if err != nil { + return nil, fmt.Errorf("could not calculate start of period: %w", err) + } + + dataPoints, err := a.sloGetter.GetSLIAvailabilityInRangeAutoStep(ctx, req.SLOID, from, to) + if err != nil { + return nil, fmt.Errorf("could not get SLI availability in range: %w", err) + } + + dataPoints, err = sanitizeDataPointsUntilEndPeriod(dataPoints, req.BudgetRangeType) + if err != nil { + return nil, fmt.Errorf("could not sanitize data points: %w", err) + } + + // Build the perfect burned data points and the real burned data points. + resp := &ListBurnedBudgetRangeResponse{} + budgetRatioPerStep := 100 - sloDetails.SLO.Objective + totalBudgetInRange := budgetRatioPerStep * float64(len(dataPoints)) + perfectAggr := totalBudgetInRange + realAggr := 0.0 + now := a.timeNowFunc().UTC() + for _, dp := range dataPoints { + perfectAggr -= budgetRatioPerStep // Perfect burn is constant. + if !dp.Missing { + // Our value its availability, not the error %, calculate what we have burned and aggregate on each step. + realAggr += (100 - dp.Value) + } + + // Only add real values until today. + realDP := model.DataPoint{TS: dp.TS, Missing: true} + if dp.TS.Before(now) { + realDP = model.DataPoint{ + TS: dp.TS, + Value: ((totalBudgetInRange - realAggr) / totalBudgetInRange) * 100, + } + } + + resp.RealBurnedDataPoints = append(resp.RealBurnedDataPoints, realDP) + + resp.PerfectBurnedDataPoints = append(resp.PerfectBurnedDataPoints, model.DataPoint{ + TS: dp.TS, + Value: (perfectAggr / totalBudgetInRange) * 100, + }) + } + + return resp, nil +} diff --git a/internal/http/backend/app/slo_test.go b/internal/http/backend/app/slo_test.go new file mode 100644 index 00000000..480120d3 --- /dev/null +++ b/internal/http/backend/app/slo_test.go @@ -0,0 +1,777 @@ +package app_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/slok/sloth/internal/http/backend/app" + "github.com/slok/sloth/internal/http/backend/model" + "github.com/slok/sloth/internal/http/backend/storage" + "github.com/slok/sloth/internal/http/backend/storage/storagemock" +) + +func TestListSLOs(t *testing.T) { + tests := map[string]struct { + mock func(m *storagemock.SLOGetter) + req app.ListSLOsRequest + expResp func() *app.ListSLOsResponse + expErr error + }{ + "Getting service SLOs successfully should return them properly.": { + req: app.ListSLOsRequest{ + FilterServiceID: "svc-1", + }, + mock: func(m *storagemock.SLOGetter) { + m.On("ListSLOInstantDetailsService", mock.Anything, "svc-1").Return([]storage.SLOInstantDetails{ + { + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 23.5, + BurnedBudgetWindowPercent: 10.0, + }, + Alerts: model.SLOAlerts{ + FiringWarning: &model.Alert{Name: "slo-1-warning"}, + }, + }, + { + SLO: model.SLO{ + ID: "slo-2", + Name: "SLO 2", + ServiceID: "svc-1", + Objective: 95.0, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-2", + BurningBudgetPercent: 50.0, + BurnedBudgetWindowPercent: 60.0, + }, + Alerts: model.SLOAlerts{ + FiringPage: &model.Alert{Name: "slo-2-critical"}, + }, + }, + }, nil) + }, + expResp: func() *app.ListSLOsResponse { + return &app.ListSLOsResponse{ + SLOs: []app.RealTimeSLODetails{ + { + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + }, + Budget: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 23.5, + BurnedBudgetWindowPercent: 10.0, + }, + Alerts: model.SLOAlerts{ + FiringWarning: &model.Alert{Name: "slo-1-warning"}, + }, + }, + { + SLO: model.SLO{ + ID: "slo-2", + Name: "SLO 2", + ServiceID: "svc-1", + Objective: 95.0, + }, + Budget: model.SLOBudgetDetails{ + SLOID: "slo-2", + BurningBudgetPercent: 50.0, + BurnedBudgetWindowPercent: 60.0, + }, + Alerts: model.SLOAlerts{ + FiringPage: &model.Alert{Name: "slo-2-critical"}, + }, + }, + }, + } + }, + }, + + "Searching service SLOs successfully should return them properly.": { + req: app.ListSLOsRequest{ + FilterSearchInput: "test", + FilterServiceID: "svc-1", + }, + mock: func(m *storagemock.SLOGetter) { + m.On("ListSLOInstantDetailsServiceBySLOSearch", mock.Anything, "svc-1", "test").Return([]storage.SLOInstantDetails{ + { + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 23.5, + BurnedBudgetWindowPercent: 10.0, + }, + Alerts: model.SLOAlerts{ + FiringWarning: &model.Alert{Name: "slo-1-warning"}, + }, + }, + { + SLO: model.SLO{ + ID: "slo-2", + Name: "SLO 2", + ServiceID: "svc-1", + Objective: 95.0, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-2", + BurningBudgetPercent: 50.0, + BurnedBudgetWindowPercent: 60.0, + }, + Alerts: model.SLOAlerts{ + FiringPage: &model.Alert{Name: "slo-2-critical"}, + }, + }, + }, nil) + }, + expResp: func() *app.ListSLOsResponse { + return &app.ListSLOsResponse{ + SLOs: []app.RealTimeSLODetails{ + { + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + }, + Budget: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 23.5, + BurnedBudgetWindowPercent: 10.0, + }, + Alerts: model.SLOAlerts{ + FiringWarning: &model.Alert{Name: "slo-1-warning"}, + }, + }, + { + SLO: model.SLO{ + ID: "slo-2", + Name: "SLO 2", + ServiceID: "svc-1", + Objective: 95.0, + }, + Budget: model.SLOBudgetDetails{ + SLOID: "slo-2", + BurningBudgetPercent: 50.0, + BurnedBudgetWindowPercent: 60.0, + }, + Alerts: model.SLOAlerts{ + FiringPage: &model.Alert{Name: "slo-2-critical"}, + }, + }, + }, + } + }, + }, + + "Getting all SLOs successfully should return them properly.": { + req: app.ListSLOsRequest{}, + mock: func(m *storagemock.SLOGetter) { + m.On("ListSLOInstantDetails", mock.Anything).Return([]storage.SLOInstantDetails{ + { + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 23.5, + BurnedBudgetWindowPercent: 10.0, + }, + Alerts: model.SLOAlerts{ + FiringWarning: &model.Alert{Name: "slo-1-warning"}, + }, + }, + { + SLO: model.SLO{ + ID: "slo-2", + Name: "SLO 2", + ServiceID: "svc-1", + Objective: 95.0, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-2", + BurningBudgetPercent: 50.0, + BurnedBudgetWindowPercent: 60.0, + }, + Alerts: model.SLOAlerts{ + FiringPage: &model.Alert{Name: "slo-2-critical"}, + }, + }, + }, nil) + }, + expResp: func() *app.ListSLOsResponse { + return &app.ListSLOsResponse{ + SLOs: []app.RealTimeSLODetails{ + { + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + }, + Budget: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 23.5, + BurnedBudgetWindowPercent: 10.0, + }, + Alerts: model.SLOAlerts{ + FiringWarning: &model.Alert{Name: "slo-1-warning"}, + }, + }, + { + SLO: model.SLO{ + ID: "slo-2", + Name: "SLO 2", + ServiceID: "svc-1", + Objective: 95.0, + }, + Budget: model.SLOBudgetDetails{ + SLOID: "slo-2", + BurningBudgetPercent: 50.0, + BurnedBudgetWindowPercent: 60.0, + }, + Alerts: model.SLOAlerts{ + FiringPage: &model.Alert{Name: "slo-2-critical"}, + }, + }, + }, + } + }, + }, + + "Searching all SLOs successfully should return them properly.": { + req: app.ListSLOsRequest{ + FilterSearchInput: "test", + }, + mock: func(m *storagemock.SLOGetter) { + m.On("ListSLOInstantDetailsBySLOSearch", mock.Anything, "test").Return([]storage.SLOInstantDetails{ + { + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 23.5, + BurnedBudgetWindowPercent: 10.0, + }, + Alerts: model.SLOAlerts{ + FiringWarning: &model.Alert{Name: "slo-1-warning"}, + }, + }, + { + SLO: model.SLO{ + ID: "slo-2", + Name: "SLO 2", + ServiceID: "svc-1", + Objective: 95.0, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-2", + BurningBudgetPercent: 50.0, + BurnedBudgetWindowPercent: 60.0, + }, + Alerts: model.SLOAlerts{ + FiringPage: &model.Alert{Name: "slo-2-critical"}, + }, + }, + }, nil) + }, + expResp: func() *app.ListSLOsResponse { + return &app.ListSLOsResponse{ + SLOs: []app.RealTimeSLODetails{ + { + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + }, + Budget: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 23.5, + BurnedBudgetWindowPercent: 10.0, + }, + Alerts: model.SLOAlerts{ + FiringWarning: &model.Alert{Name: "slo-1-warning"}, + }, + }, + { + SLO: model.SLO{ + ID: "slo-2", + Name: "SLO 2", + ServiceID: "svc-1", + Objective: 95.0, + }, + Budget: model.SLOBudgetDetails{ + SLOID: "slo-2", + BurningBudgetPercent: 50.0, + BurnedBudgetWindowPercent: 60.0, + }, + Alerts: model.SLOAlerts{ + FiringPage: &model.Alert{Name: "slo-2-critical"}, + }, + }, + }, + } + }, + }, + + "Getting service SLOs paginated should return them properly.": { + req: app.ListSLOsRequest{ + FilterServiceID: "svc-1", + Cursor: "eyJzaXplIjozMCwicGFnZSI6M30=", + }, + mock: func(m *storagemock.SLOGetter) { + // Returns all. + slos := []storage.SLOInstantDetails{} + for i := 1; i <= 200; i++ { + slos = append(slos, storage.SLOInstantDetails{ + SLO: model.SLO{ID: fmt.Sprintf("slo-%03d", i)}, + }) + } + + m.On("ListSLOInstantDetailsService", mock.Anything, "svc-1").Return(slos, nil) + }, + expResp: func() *app.ListSLOsResponse { + slos := []app.RealTimeSLODetails{} + for i := 61; i <= 90; i++ { + slos = append(slos, app.RealTimeSLODetails{ + SLO: model.SLO{ID: fmt.Sprintf("slo-%03d", i)}, + }) + } + return &app.ListSLOsResponse{ + SLOs: slos, + PaginationCursors: app.PaginationCursors{ + PrevCursor: "eyJzaXplIjozMCwicGFnZSI6Mn0=", + NextCursor: "eyJzaXplIjozMCwicGFnZSI6NH0=", + HasNext: true, + HasPrevious: true, + }, + } + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + mSLOgetter := storagemock.NewSLOGetter(t) + test.mock(mSLOgetter) + + a, err := app.NewApp(app.AppConfig{ + ServiceGetter: storagemock.NewServiceGetter(t), + SLOGetter: mSLOgetter, + }) + require.NoError(t, err) + resp, err := a.ListSLOs(context.TODO(), test.req) + + if test.expErr != nil { + assert.Error(err) + + } else if assert.NoError(err) { + assert.Equal(test.expResp(), resp) + } + }) + } +} + +func TestGetSLO(t *testing.T) { + tests := map[string]struct { + mock func(m *storagemock.SLOGetter) + req app.GetSLORequest + expResp *app.GetSLOResponse + expErr error + }{ + "Getting SLO details successfully should return them properly.": { + req: app.GetSLORequest{ + SLOID: "slo-1", + }, + mock: func(m *storagemock.SLOGetter) { + m.On("GetSLOInstantDetails", mock.Anything, "slo-1").Return(&storage.SLOInstantDetails{ + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 23.5, + BurnedBudgetWindowPercent: 10.0, + }, + Alerts: model.SLOAlerts{ + FiringWarning: &model.Alert{Name: "slo-1-warning"}, + }, + }, nil) + }, + expResp: &app.GetSLOResponse{ + SLO: app.RealTimeSLODetails{ + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + }, + Budget: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 23.5, + BurnedBudgetWindowPercent: 10.0, + }, + Alerts: model.SLOAlerts{ + FiringWarning: &model.Alert{Name: "slo-1-warning"}, + }, + }, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + mSLOgetter := storagemock.NewSLOGetter(t) + test.mock(mSLOgetter) + + a, err := app.NewApp(app.AppConfig{ + ServiceGetter: storagemock.NewServiceGetter(t), + SLOGetter: mSLOgetter, + }) + require.NoError(t, err) + resp, err := a.GetSLO(context.TODO(), test.req) + + if test.expErr != nil { + assert.Error(err) + + } else if assert.NoError(err) { + assert.Equal(test.expResp, resp) + } + }) + } +} + +func TestListSLIAvailabilityRange(t *testing.T) { + var t0, _ = time.Parse(time.RFC3339, "2025-11-14T01:02:03Z") + + tests := map[string]struct { + mock func(m *storagemock.SLOGetter) + req app.ListSLIAvailabilityRangeRequest + expResp *app.ListSLIAvailabilityRangeResponse + expErr bool + }{ + "Having a to before a from should return an error.": { + req: app.ListSLIAvailabilityRangeRequest{ + SLOID: "slo-1", + From: t0, + To: t0.Add(-1 * time.Hour), + }, + mock: func(m *storagemock.SLOGetter) {}, + expErr: true, + }, + + "Having small time range should return an error.": { + req: app.ListSLIAvailabilityRangeRequest{ + SLOID: "slo-1", + From: t0, + To: t0.Add(29 * time.Minute), + }, + mock: func(m *storagemock.SLOGetter) {}, + expErr: true, + }, + + "A from is required.": { + req: app.ListSLIAvailabilityRangeRequest{ + SLOID: "slo-1", + To: t0.Add(1 * time.Hour), + }, + mock: func(m *storagemock.SLOGetter) {}, + expErr: true, + }, + + "SLO ID is required.": { + req: app.ListSLIAvailabilityRangeRequest{ + From: t0, + To: t0.Add(1 * time.Hour), + }, + mock: func(m *storagemock.SLOGetter) {}, + expErr: true, + }, + + "Having a correct time range should return the SLO SLI availability with the proper steps.": { + req: app.ListSLIAvailabilityRangeRequest{ + SLOID: "slo-1", + From: t0, + To: t0.Add(1 * time.Hour), + }, + mock: func(m *storagemock.SLOGetter) { + m.On("GetSLIAvailabilityInRange", mock.Anything, "slo-1", t0, t0.Add(1*time.Hour), 1*time.Minute).Return([]model.DataPoint{ + {TS: t0.Add(0 * time.Minute), Value: 99.9}, + {TS: t0.Add(5 * time.Minute), Value: 99.8}, + {TS: t0.Add(10 * time.Minute), Value: 99.7}, + {TS: t0.Add(15 * time.Minute), Value: 99.6}, + {TS: t0.Add(20 * time.Minute), Value: 99.5}, + {TS: t0.Add(25 * time.Minute), Value: 99.4}, + {TS: t0.Add(30 * time.Minute), Value: 99.3}, + {TS: t0.Add(35 * time.Minute), Value: 99.2}, + {TS: t0.Add(40 * time.Minute), Value: 99.3}, + {TS: t0.Add(45 * time.Minute), Value: 99.42}, + {TS: t0.Add(50 * time.Minute), Value: 99.11}, + {TS: t0.Add(55 * time.Minute), Value: 99.78}, + {TS: t0.Add(59 * time.Minute), Value: 99.1}, + }, nil) + }, + expResp: &app.ListSLIAvailabilityRangeResponse{ + AvailabilityDataPoints: []model.DataPoint{ + {TS: t0.Add(0 * time.Minute), Value: 99.9}, + {TS: t0.Add(1 * time.Minute), Missing: true}, + {TS: t0.Add(2 * time.Minute), Missing: true}, + {TS: t0.Add(3 * time.Minute), Missing: true}, + {TS: t0.Add(4 * time.Minute), Missing: true}, + {TS: t0.Add(5 * time.Minute), Value: 99.8}, + {TS: t0.Add(6 * time.Minute), Missing: true}, + {TS: t0.Add(7 * time.Minute), Missing: true}, + {TS: t0.Add(8 * time.Minute), Missing: true}, + {TS: t0.Add(9 * time.Minute), Missing: true}, + {TS: t0.Add(10 * time.Minute), Value: 99.7}, + {TS: t0.Add(11 * time.Minute), Missing: true}, + {TS: t0.Add(12 * time.Minute), Missing: true}, + {TS: t0.Add(13 * time.Minute), Missing: true}, + {TS: t0.Add(14 * time.Minute), Missing: true}, + {TS: t0.Add(15 * time.Minute), Value: 99.6}, + {TS: t0.Add(16 * time.Minute), Missing: true}, + {TS: t0.Add(17 * time.Minute), Missing: true}, + {TS: t0.Add(18 * time.Minute), Missing: true}, + {TS: t0.Add(19 * time.Minute), Missing: true}, + {TS: t0.Add(20 * time.Minute), Value: 99.5}, + {TS: t0.Add(21 * time.Minute), Missing: true}, + {TS: t0.Add(22 * time.Minute), Missing: true}, + {TS: t0.Add(23 * time.Minute), Missing: true}, + {TS: t0.Add(24 * time.Minute), Missing: true}, + {TS: t0.Add(25 * time.Minute), Value: 99.4}, + {TS: t0.Add(26 * time.Minute), Missing: true}, + {TS: t0.Add(27 * time.Minute), Missing: true}, + {TS: t0.Add(28 * time.Minute), Missing: true}, + {TS: t0.Add(29 * time.Minute), Missing: true}, + {TS: t0.Add(30 * time.Minute), Value: 99.3}, + {TS: t0.Add(31 * time.Minute), Missing: true}, + {TS: t0.Add(32 * time.Minute), Missing: true}, + {TS: t0.Add(33 * time.Minute), Missing: true}, + {TS: t0.Add(34 * time.Minute), Missing: true}, + {TS: t0.Add(35 * time.Minute), Value: 99.2}, + {TS: t0.Add(36 * time.Minute), Missing: true}, + {TS: t0.Add(37 * time.Minute), Missing: true}, + {TS: t0.Add(38 * time.Minute), Missing: true}, + {TS: t0.Add(39 * time.Minute), Missing: true}, + {TS: t0.Add(40 * time.Minute), Value: 99.3}, + {TS: t0.Add(41 * time.Minute), Missing: true}, + {TS: t0.Add(42 * time.Minute), Missing: true}, + {TS: t0.Add(43 * time.Minute), Missing: true}, + {TS: t0.Add(44 * time.Minute), Missing: true}, + {TS: t0.Add(45 * time.Minute), Value: 99.42}, + {TS: t0.Add(46 * time.Minute), Missing: true}, + {TS: t0.Add(47 * time.Minute), Missing: true}, + {TS: t0.Add(48 * time.Minute), Missing: true}, + {TS: t0.Add(49 * time.Minute), Missing: true}, + {TS: t0.Add(50 * time.Minute), Value: 99.11}, + {TS: t0.Add(51 * time.Minute), Missing: true}, + {TS: t0.Add(52 * time.Minute), Missing: true}, + {TS: t0.Add(53 * time.Minute), Missing: true}, + {TS: t0.Add(54 * time.Minute), Missing: true}, + {TS: t0.Add(55 * time.Minute), Value: 99.78}, + {TS: t0.Add(56 * time.Minute), Missing: true}, + {TS: t0.Add(57 * time.Minute), Missing: true}, + {TS: t0.Add(58 * time.Minute), Missing: true}, + {TS: t0.Add(59 * time.Minute), Value: 99.1}, + }, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + mSLOgetter := storagemock.NewSLOGetter(t) + test.mock(mSLOgetter) + + a, err := app.NewApp(app.AppConfig{ + ServiceGetter: storagemock.NewServiceGetter(t), + SLOGetter: mSLOgetter, + }) + require.NoError(t, err) + resp, err := a.ListSLIAvailabilityRange(context.TODO(), test.req) + + if test.expErr { + assert.Error(err) + + } else if assert.NoError(err) { + assert.Equal(test.expResp, resp) + } + }) + } +} + +func TestListBurnedBudgetRange(t *testing.T) { + var t0, _ = time.Parse(time.RFC3339, "2025-11-14T01:02:03Z") + var startT0 = time.Date(t0.Year(), t0.Month(), 1, 0, 0, 0, 0, t0.Location()) + + tests := map[string]struct { + mock func(m *storagemock.SLOGetter) + req app.ListBurnedBudgetRangeRequest + expResp *app.ListBurnedBudgetRangeResponse + expErr bool + }{ + "SLO ID is required.": { + req: app.ListBurnedBudgetRangeRequest{}, + mock: func(m *storagemock.SLOGetter) {}, + expErr: true, + }, + + "Having a correct budget range should return the SLO burned range with the proper steps.": { + req: app.ListBurnedBudgetRangeRequest{ + SLOID: "slo-1", + BudgetRangeType: app.BudgetRangeTypeMonthly, + }, + mock: func(m *storagemock.SLOGetter) { + m.On("GetSLOInstantDetails", mock.Anything, "slo-1").Return(&storage.SLOInstantDetails{ + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 23.5, + BurnedBudgetWindowPercent: 10.0, + }, + Alerts: model.SLOAlerts{ + FiringWarning: &model.Alert{Name: "slo-1-warning"}, + }, + }, nil) + + m.On("GetSLIAvailabilityInRangeAutoStep", mock.Anything, "slo-1", startT0, t0).Return([]model.DataPoint{ + {TS: startT0.Add(0 * 24 * time.Hour), Value: 99.0}, + {TS: startT0.Add(1 * 24 * time.Hour), Value: 99.1}, + {TS: startT0.Add(2 * 24 * time.Hour), Value: 99.2}, + {TS: startT0.Add(3 * 24 * time.Hour), Value: 99.3}, + {TS: startT0.Add(4 * 24 * time.Hour), Value: 99.4}, + {TS: startT0.Add(5 * 24 * time.Hour), Value: 99.5}, + {TS: startT0.Add(6 * 24 * time.Hour), Value: 99.6}, + }, nil) + }, + expResp: &app.ListBurnedBudgetRangeResponse{ + RealBurnedDataPoints: []model.DataPoint{ + {TS: startT0.Add(0 * 24 * time.Hour), Value: 66.66666666666478}, + {TS: startT0.Add(1 * 24 * time.Hour), Value: 36.66666666666288}, + {TS: startT0.Add(2 * 24 * time.Hour), Value: 9.99999999999479}, + {TS: startT0.Add(3 * 24 * time.Hour), Value: -13.333333333339963}, + {TS: startT0.Add(4 * 24 * time.Hour), Value: -33.33333333334092}, + {TS: startT0.Add(5 * 24 * time.Hour), Value: -50.00000000000853}, + {TS: startT0.Add(6 * 24 * time.Hour), Value: -63.333333333342814}, + {TS: startT0.Add(7 * 24 * time.Hour), Value: -63.333333333342814}, + {TS: startT0.Add(8 * 24 * time.Hour), Value: -63.333333333342814}, + {TS: startT0.Add(9 * 24 * time.Hour), Value: -63.333333333342814}, + {TS: startT0.Add(10 * 24 * time.Hour), Value: -63.333333333342814}, + {TS: startT0.Add(11 * 24 * time.Hour), Value: -63.333333333342814}, + {TS: startT0.Add(12 * 24 * time.Hour), Value: -63.333333333342814}, + {TS: startT0.Add(13 * 24 * time.Hour), Value: -63.333333333342814}, + {TS: startT0.Add(14 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(15 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(16 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(17 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(18 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(19 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(20 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(21 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(22 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(23 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(24 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(25 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(26 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(27 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(28 * 24 * time.Hour), Missing: true}, + {TS: startT0.Add(29 * 24 * time.Hour), Missing: true}, + }, + PerfectBurnedDataPoints: []model.DataPoint{ + {TS: startT0.Add(0 * 24 * time.Hour), Value: 96.66666666666667}, + {TS: startT0.Add(1 * 24 * time.Hour), Value: 93.33333333333333}, + {TS: startT0.Add(2 * 24 * time.Hour), Value: 90}, + {TS: startT0.Add(3 * 24 * time.Hour), Value: 86.66666666666667}, + {TS: startT0.Add(4 * 24 * time.Hour), Value: 83.33333333333334}, + {TS: startT0.Add(5 * 24 * time.Hour), Value: 80}, + {TS: startT0.Add(6 * 24 * time.Hour), Value: 76.66666666666667}, + {TS: startT0.Add(7 * 24 * time.Hour), Value: 73.33333333333333}, + {TS: startT0.Add(8 * 24 * time.Hour), Value: 70}, + {TS: startT0.Add(9 * 24 * time.Hour), Value: 66.66666666666666}, + {TS: startT0.Add(10 * 24 * time.Hour), Value: 63.33333333333333}, + {TS: startT0.Add(11 * 24 * time.Hour), Value: 60}, + {TS: startT0.Add(12 * 24 * time.Hour), Value: 56.666666666666664}, + {TS: startT0.Add(13 * 24 * time.Hour), Value: 53.333333333333336}, + {TS: startT0.Add(14 * 24 * time.Hour), Value: 50}, + {TS: startT0.Add(15 * 24 * time.Hour), Value: 46.666666666666664}, + {TS: startT0.Add(16 * 24 * time.Hour), Value: 43.333333333333336}, + {TS: startT0.Add(17 * 24 * time.Hour), Value: 40}, + {TS: startT0.Add(18 * 24 * time.Hour), Value: 36.666666666666664}, + {TS: startT0.Add(19 * 24 * time.Hour), Value: 33.33333333333333}, + {TS: startT0.Add(20 * 24 * time.Hour), Value: 30}, + {TS: startT0.Add(21 * 24 * time.Hour), Value: 26.666666666666668}, + {TS: startT0.Add(22 * 24 * time.Hour), Value: 23.333333333333332}, + {TS: startT0.Add(23 * 24 * time.Hour), Value: 20}, + {TS: startT0.Add(24 * 24 * time.Hour), Value: 16.666666666666664}, + {TS: startT0.Add(25 * 24 * time.Hour), Value: 13.333333333333334}, + {TS: startT0.Add(26 * 24 * time.Hour), Value: 10}, + {TS: startT0.Add(27 * 24 * time.Hour), Value: 6.666666666666667}, + {TS: startT0.Add(28 * 24 * time.Hour), Value: 3.3333333333333335}, + {TS: startT0.Add(29 * 24 * time.Hour), Value: 0}, + }, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + mSLOgetter := storagemock.NewSLOGetter(t) + test.mock(mSLOgetter) + + a, err := app.NewApp(app.AppConfig{ + ServiceGetter: storagemock.NewServiceGetter(t), + SLOGetter: mSLOgetter, + TimeNowFunc: func() time.Time { return t0 }, + }) + require.NoError(t, err) + resp, err := a.ListBurnedBudgetRange(context.TODO(), test.req) + + if test.expErr { + assert.Error(err) + + } else if assert.NoError(err) { + assert.Equal(test.expResp, resp) + } + }) + } +} diff --git a/internal/http/backend/app/time_utils.go b/internal/http/backend/app/time_utils.go new file mode 100644 index 00000000..49cdac4b --- /dev/null +++ b/internal/http/backend/app/time_utils.go @@ -0,0 +1,123 @@ +package app + +import ( + "fmt" + "time" + + "github.com/slok/sloth/internal/http/backend/model" +) + +func calculateStepsForTimeRange(from, to time.Time) time.Duration { + const autoSteps = 50 + totalDuration := to.Sub(from) + step := totalDuration / time.Duration(autoSteps) + + // Round step to minutes. + if step < time.Minute { + step = time.Minute + } + step = (time.Duration(int(step.Minutes())) * time.Minute) + + return step +} + +func sanitizeDataPoints(dps []model.DataPoint, from, to time.Time, step time.Duration) []model.DataPoint { + // Create a map for quick lookup. + dpMap := make(map[int64]model.DataPoint) + for _, dp := range dps { + dpMap[dp.TS.Unix()] = dp + } + + // Iterate over the expected timestamps and fill gaps with missing values. + sanitizedDPs := []model.DataPoint{} + for ts := from; ts.Before(to); ts = ts.Add(step) { + unixTS := ts.Unix() + if dp, exists := dpMap[unixTS]; exists { + sanitizedDPs = append(sanitizedDPs, dp) + } else { + sanitizedDPs = append(sanitizedDPs, model.DataPoint{ + TS: ts, + Missing: true, + }) + } + } + + return sanitizedDPs +} + +func roundTimeToDay(t time.Time) time.Time { + return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) +} + +func weekMonday(t time.Time) time.Time { + diff := time.Duration(t.Weekday() - 1) + if diff < 0 { + diff = 6 + } + + return roundTimeToDay(t).Add(-1 * diff * 24 * time.Hour) // Remove the diff days until monday. +} + +func monthFirst(t time.Time) time.Time { + return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()) +} + +func quarterFirst(t time.Time) time.Time { + // Gets the first day of the quarter the time is in. + month := ((t.Month()-1)/3)*3 + 1 + return time.Date(t.Year(), month, 1, 0, 0, 0, 0, t.Location()) +} + +func yearFirst(t time.Time) time.Time { + // Gets the first day of the year the time is in. + return time.Date(t.Year(), 1, 1, 0, 0, 0, 0, t.Location()) +} + +func startOfPeriod(t time.Time, periodType BudgetRangeType) (time.Time, error) { + switch periodType { + case BudgetRangeTypeYearly: + return yearFirst(t), nil + case BudgetRangeTypeQuarterly: + return quarterFirst(t), nil + case BudgetRangeTypeMonthly: + return monthFirst(t), nil + case BudgetRangeTypeWeekly: + return weekMonday(t), nil + } + + return time.Time{}, fmt.Errorf("unknown budget range type: %q", periodType) +} + +func endOfPeriod(t time.Time, periodType BudgetRangeType) (time.Time, error) { + switch periodType { + case BudgetRangeTypeYearly: + return yearFirst(t).Add(365*24*time.Hour - 1), nil + case BudgetRangeTypeQuarterly: + // TODO: This is a simplification, not all months have 30 days. + return quarterFirst(t).Add(3*30*24*time.Hour - 1), nil + case BudgetRangeTypeMonthly: + // TODO: This is a simplification, not all months have 30 days. + return monthFirst(t).Add(30*24*time.Hour - 1), nil + case BudgetRangeTypeWeekly: + return weekMonday(t).Add(7*24*time.Hour - 1), nil + } + + return time.Time{}, fmt.Errorf("unknown budget range type: %q", periodType) +} + +func sanitizeDataPointsUntilEndPeriod(dps []model.DataPoint, periodType BudgetRangeType) ([]model.DataPoint, error) { + if len(dps) < 2 { + return nil, fmt.Errorf("not enough data points to fill time range") + } + + // Get the step. + from := dps[0].TS + step := dps[1].TS.Sub(dps[0].TS) + endPeriod, err := endOfPeriod(from, periodType) + if err != nil { + return nil, err + } + dps = sanitizeDataPoints(dps, from, endPeriod, step) + + return dps, nil +} diff --git a/internal/http/backend/model/model.go b/internal/http/backend/model/model.go new file mode 100644 index 00000000..ab0e2333 --- /dev/null +++ b/internal/http/backend/model/model.go @@ -0,0 +1,37 @@ +package model + +import "time" + +type Service struct { + ID string +} + +type SLO struct { + ID string + Name string + ServiceID string + Objective float64 + PeriodDuration time.Duration +} + +type SLOBudgetDetails struct { + SLOID string + BurningBudgetPercent float64 // Percentage of error budget burning. + BurnedBudgetWindowPercent float64 // Percentage of error budget burned in the period window. +} + +type SLOAlerts struct { + SLOID string + FiringPage *Alert + FiringWarning *Alert +} + +type Alert struct { + Name string +} + +type DataPoint struct { + Value float64 + Missing bool // Easier than using float64 nil pointers. + TS time.Time +} diff --git a/internal/http/backend/storage/fake/fake.go b/internal/http/backend/storage/fake/fake.go new file mode 100644 index 00000000..04f4634c --- /dev/null +++ b/internal/http/backend/storage/fake/fake.go @@ -0,0 +1,394 @@ +package storage + +import ( + "context" + "fmt" + "math/rand" + "time" + + "github.com/slok/sloth/internal/http/backend/model" + "github.com/slok/sloth/internal/http/backend/storage" + commonerrors "github.com/slok/sloth/pkg/common/errors" +) + +type FakeRepository struct { + services []model.Service + slos []model.SLO + sloBudgetDetails []model.SLOBudgetDetails + sloAlerts []model.SLOAlerts +} + +var ( + days30 = 30 * 24 * time.Hour +) + +func NewFakeRepository() *FakeRepository { + r := FakeRepository{} + r.genFakeData() + + return &r +} + +func (f *FakeRepository) genFakeData() { + f.services = []model.Service{ + {ID: "api-gateway"}, + {ID: "auth-service"}, + {ID: "billing-service"}, + {ID: "checkout-service"}, + {ID: "notification-service"}, + {ID: "order-service"}, + {ID: "payment-service"}, + {ID: "product-catalog"}, + {ID: "recommendation-engine"}, + {ID: "search-service"}, + {ID: "user-service"}, + {ID: "warehouse-service"}, + {ID: "payment-service"}, + {ID: "reporting-service"}, + {ID: "analytics-service"}, + {ID: "shipping-service"}, + {ID: "inventory-service"}, + {ID: "customer-service"}, + {ID: "review-service"}, + {ID: "loyalty-service"}, + {ID: "discount-service"}, + {ID: "fraud-detection-service"}, + {ID: "email-service"}, + {ID: "sms-service"}, + {ID: "push-notification-service"}, + {ID: "content-management-service"}, + {ID: "media-service"}, + {ID: "search-indexer-service"}, + {ID: "data-warehouse-service"}, + {ID: "etl-service"}, + {ID: "ad-serving-service"}, + {ID: "data-processing-service"}, + {ID: "real-time-analytics-service"}, + {ID: "session-management-service"}, + {ID: "api-rate-limiting-service"}, + {ID: "load-balancing-service"}, + {ID: "caching-service"}, + {ID: "logging-service"}, + {ID: "monitoring-service"}, + {ID: "alerting-service"}, + {ID: "backup-service"}, + {ID: "disaster-recovery-service"}, + {ID: "configuration-service"}, + {ID: "feature-flag-service"}, + {ID: "ab-testing-service"}, + {ID: "user-profile-service"}, + {ID: "session-storage-service"}, + {ID: "oauth-service"}, + {ID: "saml-service"}, + {ID: "openid-connect-service"}, + {ID: "two-factor-authentication-service"}, + {ID: "password-reset-service"}, + {ID: "account-management-service"}, + {ID: "billing-integration-service"}, + {ID: "tax-calculation-service"}, + {ID: "shipping-integration-service"}, + {ID: "third-party-api-integration-service"}, + {ID: "webhook-service"}, + {ID: "chat-service"}, + {ID: "video-conferencing-service"}, + {ID: "file-storage-service"}, + {ID: "image-processing-service"}, + {ID: "pdf-generation-service"}, + {ID: "search-optimization-service"}, + {ID: "performance-monitoring-service"}, + {ID: "user-behavior-tracking-service"}, + {ID: "heatmap-service"}, + {ID: "session-replay-service"}, + {ID: "conversion-tracking-service"}, + {ID: "funnel-analysis-service"}, + {ID: "customer-segmentation-service"}, + {ID: "a-b-testing-analytics-service"}, + {ID: "recommendation-algorithm-service"}, + {ID: "personalization-service"}, + {ID: "search-personalization-service"}, + {ID: "dynamic-content-service"}, + {ID: "real-time-bidding-service"}, + {ID: "ad-targeting-service"}, + {ID: "ad-frequency-capping-service"}, + {ID: "ad-performance-tracking-service"}, + {ID: "campaign-management-service"}, + {ID: "budget-optimization-service"}, + {ID: "bid-management-service"}, + {ID: "creative-management-service"}, + {ID: "audience-insights-service"}, + {ID: "marketplace-integration-service"}, + {ID: "affiliate-marketing-service"}, + {ID: "influencer-marketing-service"}, + {ID: "content-distribution-service"}, + {ID: "cdn-integration-service"}, + {ID: "video-streaming-service"}, + {ID: "live-broadcasting-service"}, + {ID: "virtual-reality-service"}, + {ID: "augmented-reality-service"}, + {ID: "blockchain-integration-service"}, + {ID: "cryptocurrency-payment-service"}, + {ID: "nft-management-service"}, + {ID: "smart-contract-service"}, + {ID: "iot-integration-service"}, + {ID: "edge-computing-service"}, + {ID: "fog-computing-service"}, + {ID: "quantum-computing-service"}, + {ID: "ai-integration-service"}, + {ID: "machine-learning-service"}, + {ID: "deep-learning-service"}, + {ID: "natural-language-processing-service"}, + {ID: "computer-vision-service"}, + {ID: "speech-recognition-service"}, + {ID: "chatbot-service"}, + {ID: "virtual-assistant-service"}, + {ID: "robotic-process-automation-service"}, + {ID: "process-mining-service"}, + {ID: "business-intelligence-service"}, + {ID: "data-visualization-service"}, + {ID: "predictive-analytics-service"}, + {ID: "prescriptive-analytics-service"}, + {ID: "data-governance-service"}, + {ID: "data-quality-service"}, + {ID: "master-data-management-service"}, + {ID: "metadata-management-service"}, + {ID: "data-lineage-service"}, + {ID: "compliance-management-service"}, + {ID: "risk-management-service"}, + {ID: "fraud-prevention-service"}, + {ID: "cybersecurity-service"}, + {ID: "identity-and-access-management-service"}, + {ID: "security-information-and-event-management-service"}, + {ID: "vulnerability-management-service"}, + {ID: "penetration-testing-service"}, + {ID: "incident-response-service"}, + {ID: "disaster-recovery-planning-service"}, + {ID: "business-continuity-planning-service"}, + } + + slos := []model.SLO{} + objectiveValues := []float64{ + 99, 95, 97, 98, 96, 99.9, 92, 99.5, 99.99, 80, 85, 99, + 99.97, 94.5, 96.7, 98.3, 97.5, 99.2, 93.8, 99.8, + 90, 88.5, 91.2, 94.8, 97.1, 95.6, 99.3, 96.4, 98.7, 92.9, + 99.6, 97.9, 94.2, 95.3, 98.1, 93.5, 99.4, 96.8, 97.6, 91.7, + 89.9, 92.5, 94.1, 95.8, 98.9, 99.1, 97.3, 96.2, 93.7, + 99.85, 98.5, 97.8, 95.4, 94.6, 92.3, 90.7, 88.9, 91.5, 93.2, + 96.9, 99.7, 98.2, 97.4, 95.1, 94.3, 92.8, 90.5, 89.2, + 99.88, 98.6, 97.2, 95.5, 94.7, 93.1, 91.9, 90.3, 88.7, + 99.93, 98.8, 97.0, 95.2, 94.4, 92.6, 91.1, 89.5, + 99, 95, 97, 98, 96, 99.9, 92, 99.5, 99.99, 80, 85, 99, + 99.97, 94.5, 96.7, 98.3, 97.5, 99.2, 93.8, 99.8, + 90, 88.5, 91.2, 94.8, 97.1, 95.6, 99.3, 96.4, 98.7, 92.9, + 99.6, 97.9, 94.2, 95.3, 98.1, 93.5, 99.4, 96.8, 97.6, 91.7, + 89.9, 92.5, 94.1, 95.8, 98.9, 99.1, 97.3, 96.2, 93.7, + 99.85, 98.5, 97.8, 95.4, 94.6, 92.3, 90.7, 88.9, 91.5, 93.2, + 96.9, 99.7, 98.2, 97.4, 95.1, 94.3, 92.8, 90.5, 89.2, + 99.88, 98.6, 97.2, 95.5, 94.7, 93.1, 91.9, 90.3, 88.7, + 99.93, 98.8, 97.0, 95.2, 94.4, 92.6, 91.1, 89.5, + } + for i, svc := range f.services { + sloQuantity := i * 456 % len(objectiveValues) + if sloQuantity == 0 { + sloQuantity = 2 + } + for j, obj := range objectiveValues[:sloQuantity] { + sloName := fmt.Sprintf("slo-%04d-%04d", i, j) + slos = append(slos, model.SLO{ + ID: svc.ID + "-" + sloName, + Name: sloName, + ServiceID: svc.ID, + Objective: obj, + PeriodDuration: days30, + }) + } + } + f.slos = slos + + allSLOBudgets := []model.SLOBudgetDetails{} + budgetCatalog := []model.SLOBudgetDetails{ + {BurningBudgetPercent: 93.5, BurnedBudgetWindowPercent: 40}, + {BurningBudgetPercent: 71, BurnedBudgetWindowPercent: 30.2}, + {BurningBudgetPercent: 90, BurnedBudgetWindowPercent: 71}, + {BurningBudgetPercent: 292, BurnedBudgetWindowPercent: 150}, + {BurningBudgetPercent: 191, BurnedBudgetWindowPercent: 101.2}, + {BurningBudgetPercent: 622, BurnedBudgetWindowPercent: 225.1}, + {BurningBudgetPercent: 33, BurnedBudgetWindowPercent: 20.9}, + {BurningBudgetPercent: 32, BurnedBudgetWindowPercent: 25.1}, + {BurningBudgetPercent: 174, BurnedBudgetWindowPercent: 135.8}, + {BurningBudgetPercent: 89, BurnedBudgetWindowPercent: 42}, + {BurningBudgetPercent: 150, BurnedBudgetWindowPercent: 80.5}, + {BurningBudgetPercent: 87, BurnedBudgetWindowPercent: 33.3}, + {BurningBudgetPercent: 86, BurnedBudgetWindowPercent: 25.6}, + {BurningBudgetPercent: 88, BurnedBudgetWindowPercent: 57.9}, + {BurningBudgetPercent: 187, BurnedBudgetWindowPercent: 130}, + {BurningBudgetPercent: 85, BurnedBudgetWindowPercent: 12.89}, + {BurningBudgetPercent: 3, BurnedBudgetWindowPercent: 1.2}, + {BurningBudgetPercent: 881, BurnedBudgetWindowPercent: 0}, + {BurningBudgetPercent: 79, BurnedBudgetWindowPercent: 12.3}, + {BurningBudgetPercent: 77, BurnedBudgetWindowPercent: 0.5}, + {BurningBudgetPercent: 76, BurnedBudgetWindowPercent: 5.6}, + {BurningBudgetPercent: 75, BurnedBudgetWindowPercent: 110.7}, + {BurningBudgetPercent: 174, BurnedBudgetWindowPercent: 135.8}, + {BurningBudgetPercent: 33, BurnedBudgetWindowPercent: 20.9}, + {BurningBudgetPercent: 32, BurnedBudgetWindowPercent: 25.1}, + {BurningBudgetPercent: 622, BurnedBudgetWindowPercent: 225.1}, + {BurningBudgetPercent: 21, BurnedBudgetWindowPercent: 130.2}, + {BurningBudgetPercent: 15, BurnedBudgetWindowPercent: 10.5}, + {BurningBudgetPercent: 14, BurnedBudgetWindowPercent: 5.6}, + } + for i, slo := range f.slos { + b := budgetCatalog[i%len(budgetCatalog)] + allSLOBudgets = append(allSLOBudgets, model.SLOBudgetDetails{ + SLOID: slo.ID, + BurningBudgetPercent: b.BurningBudgetPercent, + BurnedBudgetWindowPercent: b.BurnedBudgetWindowPercent, + }) + } + f.sloBudgetDetails = allSLOBudgets + + f.sloAlerts = []model.SLOAlerts{ + {SLOID: f.slos[0].ID, FiringPage: &model.Alert{Name: "api-gateway-page"}}, + {SLOID: f.slos[90].ID, FiringWarning: &model.Alert{Name: "auth-service-warning"}}, + {SLOID: f.slos[5].ID}, + {SLOID: f.slos[36].ID, FiringPage: &model.Alert{Name: "checkout-service-page"}, FiringWarning: &model.Alert{Name: "checkout-service-warning"}}, + {SLOID: f.slos[1].ID, FiringPage: &model.Alert{Name: "order-service-page"}}, + {SLOID: f.slos[6].ID}, + {SLOID: f.slos[121].ID, FiringWarning: &model.Alert{Name: "payment-service-warning"}}, + {SLOID: f.slos[10].ID, FiringWarning: &model.Alert{Name: "product-catalog-warning"}}, + {SLOID: f.slos[67].ID, FiringPage: &model.Alert{Name: "recommendation-engine-page"}, FiringWarning: &model.Alert{Name: "recommendation-engine-warning"}}, + {SLOID: f.slos[35].ID, FiringWarning: &model.Alert{Name: "search-service-warning"}}, + {SLOID: f.slos[83].ID, FiringPage: &model.Alert{Name: "user-service-page"}}, + {SLOID: f.slos[102].ID, FiringWarning: &model.Alert{Name: "warehouse-service-warning"}}, + } +} + +func (f FakeRepository) ListAllServiceAndAlerts(ctx context.Context) ([]storage.ServiceAndAlerts, error) { + data := make([]storage.ServiceAndAlerts, 0, len(f.services)) + for _, svc := range f.services { + svcAlerts := make([]model.SLOAlerts, 0) + for _, slo := range f.slos { + if slo.ServiceID != svc.ID { + continue + } + for _, sloAlert := range f.sloAlerts { + if sloAlert.SLOID != slo.ID { + continue + } + svcAlerts = append(svcAlerts, sloAlert) + } + } + data = append(data, storage.ServiceAndAlerts{ + Service: svc, + Alerts: svcAlerts, + }) + } + + return data, nil +} + +func (f FakeRepository) ListServiceAndAlertsByServiceSearch(ctx context.Context, serviceSearchInput string) ([]storage.ServiceAndAlerts, error) { + return nil, fmt.Errorf("search not supported on storage") +} + +func (f FakeRepository) ListSLOInstantDetailsService(ctx context.Context, serviceID string) ([]storage.SLOInstantDetails, error) { + data := make([]storage.SLOInstantDetails, 0, len(f.slos)) + all, err := f.ListSLOInstantDetails(ctx) + if err != nil { + return nil, err + } + for _, sloDetail := range all { + if sloDetail.SLO.ServiceID != serviceID { + continue + } + data = append(data, sloDetail) + } + return data, nil +} + +func (f FakeRepository) ListSLOInstantDetailsServiceBySLOSearch(ctx context.Context, serviceID, sloSearchInput string) ([]storage.SLOInstantDetails, error) { + return nil, fmt.Errorf("search not supported on storage") +} + +func (f FakeRepository) ListSLOInstantDetails(ctx context.Context) ([]storage.SLOInstantDetails, error) { + data := make([]storage.SLOInstantDetails, 0, len(f.slos)) + for _, slo := range f.slos { + var budgetDetails model.SLOBudgetDetails + for _, bd := range f.sloBudgetDetails { + if bd.SLOID == slo.ID { + budgetDetails = bd + break + } + } + var alerts model.SLOAlerts + for _, alert := range f.sloAlerts { + if alert.SLOID == slo.ID { + alerts = alert + break + } + } + data = append(data, storage.SLOInstantDetails{ + SLO: slo, + BudgetDetails: budgetDetails, + Alerts: alerts, + }) + } + return data, nil +} + +func (f FakeRepository) ListSLOInstantDetailsBySLOSearch(ctx context.Context, sloSearchInput string) ([]storage.SLOInstantDetails, error) { + return nil, fmt.Errorf("search not supported on storage") +} + +func (f FakeRepository) GetSLOInstantDetails(ctx context.Context, sloID string) (*storage.SLOInstantDetails, error) { + for _, slo := range f.slos { + if slo.ID != sloID { + continue + } + var budgetDetails model.SLOBudgetDetails + for _, bd := range f.sloBudgetDetails { + if bd.SLOID == slo.ID { + budgetDetails = bd + break + } + } + var alerts model.SLOAlerts + for _, alert := range f.sloAlerts { + if alert.SLOID == slo.ID { + alerts = alert + break + } + } + return &storage.SLOInstantDetails{ + SLO: slo, + BudgetDetails: budgetDetails, + Alerts: alerts, + }, nil + } + return nil, commonerrors.ErrNotFound +} + +func (f FakeRepository) GetSLIAvailabilityInRange(ctx context.Context, sloID string, from, to time.Time, step time.Duration) ([]model.DataPoint, error) { + slo, err := f.GetSLOInstantDetails(ctx, sloID) + if err != nil { + return nil, err + } + + // Fake data. + factor := rand.Float64() + rand.Float64() + rand.Float64() + dataPoints := []model.DataPoint{} + for ts := from; ts.Before(to); ts = ts.Add(step) { + perfectBurn := 100 - slo.SLO.Objective + burned := perfectBurn * rand.Float64() * factor + dataPoints = append(dataPoints, model.DataPoint{ + Value: 100 - burned, + TS: ts, + }) + } + + return dataPoints, nil +} + +func (f FakeRepository) GetSLIAvailabilityInRangeAutoStep(ctx context.Context, sloID string, from, to time.Time) ([]model.DataPoint, error) { + autoStep := to.Sub(from) / 120 + return f.GetSLIAvailabilityInRange(ctx, sloID, from, to, autoStep) +} diff --git a/internal/http/backend/storage/prometheus/cache.go b/internal/http/backend/storage/prometheus/cache.go new file mode 100644 index 00000000..e57cde58 --- /dev/null +++ b/internal/http/backend/storage/prometheus/cache.go @@ -0,0 +1,367 @@ +package prometheus + +import ( + "context" + "fmt" + "slices" + "strconv" + "strings" + "time" + + prommodel "github.com/prometheus/common/model" + "github.com/slok/sloth/internal/http/backend/model" + "github.com/slok/sloth/pkg/common/conventions" + slothmodel "github.com/slok/sloth/pkg/common/model" + utilsprom "github.com/slok/sloth/pkg/common/utils/prometheus" +) + +type cache struct { + SLODetailsByService map[string][]model.SLO + SLOAlertsBySLO map[string]model.SLOAlerts + BudgetDetailsBySLO map[string]model.SLOBudgetDetails + SLOIDs []string + SLOSLIWindows map[string][]time.Duration +} + +func (r *Repository) refreshCaches(ctx context.Context) error { + r.logger.Debugf("Refreshing background Prometheus caches") + + sloDetails, err := r.listSLODetails(ctx) + if err != nil { + return fmt.Errorf("could not list slo details: %w", err) + } + + sloIDs := []string{} + sloByService := map[string][]model.SLO{} + for _, slo := range sloDetails { + sloIDs = append(sloIDs, slo.ID) + sloByService[slo.ServiceID] = append(sloByService[slo.ServiceID], slo) + } + + sloAlerts, err := r.listSLOAlerts(ctx) + if err != nil { + return fmt.Errorf("could not list slo alerts: %w", err) + } + + alertsBySLO := map[string]model.SLOAlerts{} + for _, sloAlert := range sloAlerts { + _, ok := alertsBySLO[sloAlert.SLOID] + if ok { + r.logger.Warningf("SLO alerts received duplicated for slo %q", sloAlert.SLOID) + continue + } + alertsBySLO[sloAlert.SLOID] = sloAlert + } + + sloBudgets, err := r.listSLOBudgets(ctx, sloIDs) + if err != nil { + return fmt.Errorf("could not list slo budgets: %w", err) + } + + sloSLIWindows, err := r.inferSLIWindows(ctx, sloIDs) + if err != nil { + return fmt.Errorf("could not infer slo sli windows: %w", err) + } + + // Update cache. + r.mu.Lock() + r.cache.SLODetailsByService = sloByService + r.cache.SLOAlertsBySLO = alertsBySLO + r.cache.BudgetDetailsBySLO = sloBudgets + r.cache.SLOIDs = sloIDs + r.cache.SLOSLIWindows = sloSLIWindows + r.mu.Unlock() + + return nil +} + +func (r *Repository) listSLODetails(ctx context.Context) ([]model.SLO, error) { + // We will need some data first. + periodsBySLO, err := r.listSLOPeriods(ctx) + if err != nil { + return nil, fmt.Errorf("could not list slo periods: %w", err) + } + + query := fmt.Sprintf(`max(%s{%[2]s!=""}) by (%[3]s, %[2]s, %[4]s, %[5]s)`, + conventions.PromMetaSLOInfoMetric, + conventions.PromSLOIDLabelName, + conventions.PromSLOServiceLabelName, + conventions.PromSLOObjectiveLabelName, + conventions.PromSLONameLabelName, + ) + result, warnings, err := r.promcli.Query(ctx, query, r.timeNowFunc()) + if err != nil { + return nil, fmt.Errorf("could not query prometheus: %w", err) + } + + for _, warning := range warnings { + r.logger.Warningf("Prometheus query warning: %v", warning) + } + + // Parse the result vector to extract SLO details. + vector, ok := result.(prommodel.Vector) + if !ok { + return nil, fmt.Errorf("unexpected result type: %T", result) + } + + // Extract SLO details from labels. + slos := make([]model.SLO, 0, len(vector)) + for _, sample := range vector { + sloID := string(sample.Metric[conventions.PromSLOIDLabelName]) + if sloID == "" { + continue + } + + serviceName := string(sample.Metric[conventions.PromSLOServiceLabelName]) + sloName := string(sample.Metric[conventions.PromSLONameLabelName]) + objective := string(sample.Metric[conventions.PromSLOObjectiveLabelName]) + + objectiveF, err := strconv.ParseFloat(objective, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse objective %q", objective) + } + + period, ok := periodsBySLO[sloID] + if !ok { + r.logger.Warningf("Could not find period duration for SLO %q, defaulting to 30 days", sloID) + period = 30 * 24 * time.Hour + } + + slos = append(slos, model.SLO{ + ID: sloID, + Name: sloName, + ServiceID: serviceName, + Objective: objectiveF, + PeriodDuration: period, + }) + } + + return slos, nil +} + +func (r *Repository) listSLOPeriods(ctx context.Context) (map[string]time.Duration, error) { + query := fmt.Sprintf(`max(%s{%[2]s!=""}) by (%[2]s)`, conventions.PromMetaSLOTimePeriodDaysMetric, conventions.PromSLOIDLabelName) + result, warnings, err := r.promcli.Query(ctx, query, r.timeNowFunc()) + if err != nil { + return nil, fmt.Errorf("could not query prometheus: %w", err) + } + + for _, warning := range warnings { + r.logger.Warningf("Prometheus query warning: %v", warning) + } + + vector, ok := result.(prommodel.Vector) + if !ok { + return nil, fmt.Errorf("unexpected result type: %T", result) + } + + periods := make(map[string]time.Duration, len(vector)) + for _, sample := range vector { + sloID := string(sample.Metric[conventions.PromSLOIDLabelName]) + if sloID == "" { + continue + } + + // The value represents the number of days. + days := float64(sample.Value) + if days <= 0 { + r.logger.Warningf("Invalid time period days value %f for SLO %q", days, sloID) + continue + } + + // Convert days to time.Duration. + periodDuration := time.Duration(days * 24 * float64(time.Hour)) + periods[sloID] = periodDuration + } + + return periods, nil +} + +func (r *Repository) listSLOAlerts(ctx context.Context) ([]model.SLOAlerts, error) { + query := fmt.Sprintf(`max(ALERTS{%[1]s!=""}) by (alertname, %[1]s, alertstate, %[2]s)`, + conventions.PromSLOIDLabelName, + conventions.PromSLOSeverityLabelName) + result, warnings, err := r.promcli.Query(ctx, query, r.timeNowFunc()) + if err != nil { + return nil, fmt.Errorf("could not query prometheus: %w", err) + } + + for _, warning := range warnings { + r.logger.Warningf("Prometheus query warning: %v", warning) + } + + vector, ok := result.(prommodel.Vector) + if !ok { + return nil, fmt.Errorf("unexpected result type: %T", result) + } + + // Group alerts by SLO ID. + alertsBySLO := map[string]model.SLOAlerts{} + for _, sample := range vector { + sloID := string(sample.Metric[conventions.PromSLOIDLabelName]) + alertName := string(sample.Metric["alertname"]) + alertState := string(sample.Metric["alertstate"]) + severity := string(sample.Metric[conventions.PromSLOSeverityLabelName]) + + // Only process firing alerts (non-firing alerts are represented as nil pointers). + if alertState != "firing" { + continue + } + + sloAlerts, ok := alertsBySLO[sloID] + if !ok { + sloAlerts = model.SLOAlerts{SLOID: sloID} + } + + // Assign to appropriate alert field based on severity. + alert := &model.Alert{ + Name: alertName, + } + + switch severity { + case slothmodel.PageAlertSeverity.String(): + sloAlerts.FiringPage = alert + case slothmodel.TicketAlertSeverity.String(): + sloAlerts.FiringWarning = alert + default: + continue + } + + alertsBySLO[sloID] = sloAlerts + } + + alerts := []model.SLOAlerts{} + for _, a := range alertsBySLO { + alerts = append(alerts, a) + } + slices.SortStableFunc(alerts, func(x, y model.SLOAlerts) int { return strings.Compare(x.SLOID, y.SLOID) }) + + return alerts, nil +} + +func (r *Repository) listSLOBurnedPeriodRollingWindowRatio(ctx context.Context) (map[string]float64, error) { + query := fmt.Sprintf(`max(%s{%[2]s!=""}) by (%[2]s)`, conventions.PromMetaSLOPeriodErrorBudgetRemainingRatioMetric, conventions.PromSLOIDLabelName) + result, warnings, err := r.promcli.Query(ctx, query, r.timeNowFunc()) + if err != nil { + return nil, fmt.Errorf("could not query prometheus: %w", err) + } + + for _, warning := range warnings { + r.logger.Warningf("Prometheus query warning: %v", warning) + } + + vector, ok := result.(prommodel.Vector) + if !ok { + return nil, fmt.Errorf("unexpected result type: %T", result) + } + + ratioBySLO := make(map[string]float64, len(vector)) + for _, sample := range vector { + sloID := string(sample.Metric[conventions.PromSLOIDLabelName]) + if sloID == "" { + continue + } + ratioBySLO[sloID] = 1 - float64(sample.Value) // We don't want remaining, we want whats burned + } + + return ratioBySLO, nil +} + +func (r *Repository) listSLOBurningCurrentRatio(ctx context.Context) (map[string]float64, error) { + query := fmt.Sprintf(`max(%s{%[2]s!=""}) by (%[2]s)`, conventions.PromMetaSLOCurrentBurnRateRatioMetric, conventions.PromSLOIDLabelName) + result, warnings, err := r.promcli.Query(ctx, query, r.timeNowFunc()) + if err != nil { + return nil, fmt.Errorf("could not query prometheus: %w", err) + } + + for _, warning := range warnings { + r.logger.Warningf("Prometheus query warning: %v", warning) + } + + vector, ok := result.(prommodel.Vector) + if !ok { + return nil, fmt.Errorf("unexpected result type: %T", result) + } + + burnRates := make(map[string]float64, len(vector)) + for _, sample := range vector { + sloID := string(sample.Metric[conventions.PromSLOIDLabelName]) + if sloID == "" { + continue + } + burnRates[sloID] = float64(sample.Value) + } + + return burnRates, nil +} + +func (r *Repository) listSLOBudgets(ctx context.Context, sloIDs []string) (map[string]model.SLOBudgetDetails, error) { + burnedRolledWindowRatioBySLO, err := r.listSLOBurnedPeriodRollingWindowRatio(ctx) + if err != nil { + return nil, fmt.Errorf("could not list slo burned period rolling window ratio: %w", err) + } + + currentBurnRatioBySLO, err := r.listSLOBurningCurrentRatio(ctx) + if err != nil { + return nil, fmt.Errorf("could not list slo current burn ratio: %w", err) + } + + budgets := map[string]model.SLOBudgetDetails{} + for _, sloID := range sloIDs { + currentBurnRatio := currentBurnRatioBySLO[sloID] + windowBurnRatio := burnedRolledWindowRatioBySLO[sloID] + + budgets[sloID] = model.SLOBudgetDetails{ + SLOID: sloID, + BurningBudgetPercent: currentBurnRatio * 100, + BurnedBudgetWindowPercent: windowBurnRatio * 100, + } + } + + return budgets, nil +} + +// inferSLIWindows tries to infer the SLI windows of SLOs based on the SLI error recording rules. +func (r *Repository) inferSLIWindows(ctx context.Context, sloIDs []string) (map[string][]time.Duration, error) { + query := fmt.Sprintf(`count({__name__=~"^%s.*"}) by (__name__, %s)`, conventions.PromSLIErrorMetric, conventions.PromSLOIDLabelName) + sloWindows := map[string][]time.Duration{} + + result, warnings, err := r.promcli.Query(ctx, query, r.timeNowFunc()) + if err != nil { + return nil, fmt.Errorf("could not query prometheus: %w", err) + } + + for _, warning := range warnings { + r.logger.Warningf("Prometheus query warning: %v", warning) + } + + vector, ok := result.(prommodel.Vector) + if !ok { + return nil, fmt.Errorf("unexpected result type: %T", result) + } + + for _, sample := range vector { + sloID := string(sample.Metric[conventions.PromSLOIDLabelName]) + metricName := string(sample.Metric["__name__"]) + + if !slices.Contains(sloIDs, sloID) { + continue + } + + // Extract window from metric name suffix. + windowStr := strings.TrimPrefix(metricName, conventions.PromSLIErrorMetric) + windowDur, err := utilsprom.PromStrToTimeDuration(windowStr) + if err != nil { + r.logger.Warningf("Could not parse SLI window duration from metric name %q: %v", metricName, err) + continue + } + + sloWindows[sloID] = append(sloWindows[sloID], windowDur) + } + + for _, windows := range sloWindows { + slices.Sort(windows) + } + + return sloWindows, nil +} diff --git a/internal/http/backend/storage/prometheus/prometheus.go b/internal/http/backend/storage/prometheus/prometheus.go new file mode 100644 index 00000000..abddbb1b --- /dev/null +++ b/internal/http/backend/storage/prometheus/prometheus.go @@ -0,0 +1,265 @@ +package prometheus + +import ( + "context" + "fmt" + "slices" + "strings" + "sync" + "time" + + prometheusv1 "github.com/prometheus/client_golang/api/prometheus/v1" + prommodel "github.com/prometheus/common/model" + + "github.com/slok/sloth/internal/http/backend/model" + "github.com/slok/sloth/internal/http/backend/storage" + "github.com/slok/sloth/internal/log" + "github.com/slok/sloth/pkg/common/conventions" + commonerrors "github.com/slok/sloth/pkg/common/errors" +) + +// PrometheusAPIClient is an interface that defines the methods we use from the Prometheus client. +// We define it so we can add flexibility like easily mocking in tests or wrap it for functionality. +type PrometheusAPIClient interface { + prometheusv1.API +} + +type RepositoryConfig struct { + PrometheusClient PrometheusAPIClient + CacheRefreshInterval time.Duration + TimeNowFunc func() time.Time // Used for faking time in testing. + Logger log.Logger +} + +func (c *RepositoryConfig) defaults() error { + if c.PrometheusClient == nil { + return fmt.Errorf("prometheus client is required") + } + + if c.Logger == nil { + c.Logger = log.Noop + } + + if c.CacheRefreshInterval <= 0 { + c.CacheRefreshInterval = 1 * time.Minute + } + + if c.TimeNowFunc == nil { + c.TimeNowFunc = time.Now + } + + c.Logger = c.Logger.WithValues(log.Kv{"svc": "storage.prometheus.repository"}) + + return nil +} + +type Repository struct { + promcli PrometheusAPIClient + CacheRefreshInterval time.Duration + logger log.Logger + timeNowFunc func() time.Time + + cache cache + mu sync.RWMutex +} + +func NewRepository(ctx context.Context, config RepositoryConfig) (*Repository, error) { + if err := config.defaults(); err != nil { + return nil, err + } + + r := &Repository{ + promcli: config.PrometheusClient, + CacheRefreshInterval: config.CacheRefreshInterval, + timeNowFunc: config.TimeNowFunc, + logger: config.Logger, + } + + // Warm caches. + err := r.refreshCaches(ctx) + if err != nil { + return nil, fmt.Errorf("could not warm caches: %w", err) + } + + // Trigger background refresh caches. + go func() { + for { + select { + case <-ctx.Done(): + r.logger.Infof("Stopping cache refresh") + return + case <-time.After(r.CacheRefreshInterval): + err := r.refreshCaches(ctx) + if err != nil { + r.logger.Errorf("Could not refresh caches: %v", err) + } + } + } + }() + + return r, nil +} + +func (r *Repository) ListAllServiceAndAlerts(ctx context.Context) ([]storage.ServiceAndAlerts, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + result := []storage.ServiceAndAlerts{} + for serviceID, slos := range r.cache.SLODetailsByService { + sloAlertsList := []model.SLOAlerts{} + for _, slo := range slos { + alerts, ok := r.cache.SLOAlertsBySLO[slo.ID] + if ok { + // TODO: Deep copy alerts. + sloAlertsList = append(sloAlertsList, alerts) + } + } + + result = append(result, storage.ServiceAndAlerts{ + Service: model.Service{ + ID: serviceID, + }, + Alerts: sloAlertsList, + }) + } + + slices.SortStableFunc(result, func(x, y storage.ServiceAndAlerts) int { return strings.Compare(x.Service.ID, y.Service.ID) }) + + return result, nil +} + +func (r *Repository) ListServiceAndAlertsByServiceSearch(ctx context.Context, serviceSearchInput string) ([]storage.ServiceAndAlerts, error) { + return nil, fmt.Errorf("search not supported on storage") +} + +func (r *Repository) ListSLOInstantDetailsService(ctx context.Context, serviceID string) ([]storage.SLOInstantDetails, error) { + details := []storage.SLOInstantDetails{} + for _, slo := range r.cache.SLODetailsByService[serviceID] { + details = append(details, storage.SLOInstantDetails{ + SLO: slo, + BudgetDetails: r.cache.BudgetDetailsBySLO[slo.ID], + Alerts: r.cache.SLOAlertsBySLO[slo.ID], + }) + } + + slices.SortStableFunc(details, func(x, y storage.SLOInstantDetails) int { return strings.Compare(x.SLO.ID, y.SLO.ID) }) + + return details, nil +} + +func (r *Repository) ListSLOInstantDetailsServiceBySLOSearch(ctx context.Context, serviceID, sloSearchInput string) ([]storage.SLOInstantDetails, error) { + return nil, fmt.Errorf("search not supported on storage") +} + +func (r *Repository) ListSLOInstantDetails(ctx context.Context) ([]storage.SLOInstantDetails, error) { + details := []storage.SLOInstantDetails{} + for _, slos := range r.cache.SLODetailsByService { + for _, slo := range slos { + details = append(details, storage.SLOInstantDetails{ + SLO: slo, + BudgetDetails: r.cache.BudgetDetailsBySLO[slo.ID], + Alerts: r.cache.SLOAlertsBySLO[slo.ID], + }) + } + } + + slices.SortStableFunc(details, func(x, y storage.SLOInstantDetails) int { return strings.Compare(x.SLO.ID, y.SLO.ID) }) + + return details, nil +} + +func (r *Repository) ListSLOInstantDetailsBySLOSearch(ctx context.Context, sloSearchInput string) ([]storage.SLOInstantDetails, error) { + return nil, fmt.Errorf("search not supported on storage") +} + +func (r *Repository) GetSLOInstantDetails(ctx context.Context, sloID string) (*storage.SLOInstantDetails, error) { + for _, slos := range r.cache.SLODetailsByService { + for _, slo := range slos { + if slo.ID != sloID { + continue + } + + return &storage.SLOInstantDetails{ + SLO: slo, + BudgetDetails: r.cache.BudgetDetailsBySLO[slo.ID], + Alerts: r.cache.SLOAlertsBySLO[slo.ID], + }, nil + } + } + + return nil, commonerrors.ErrNotFound +} + +func (r *Repository) GetSLIAvailabilityInRange(ctx context.Context, sloID string, from, to time.Time, step time.Duration) ([]model.DataPoint, error) { + // Get the SLI shortest window for the SLO. + windows, ok := r.cache.SLOSLIWindows[sloID] + if !ok || len(windows) == 0 { + // Most probably that does not exist yet. + r.logger.Warningf("Could not find SLI windows for SLO ID %q", sloID) + return nil, nil + } + metric := conventions.GetSLIErrorMetric(windows[0]) // Use shortest window. + + return r.getSLIAvailabilityInRange(ctx, sloID, from, to, step, metric) +} + +func (r *Repository) GetSLIAvailabilityInRangeAutoStep(ctx context.Context, sloID string, from, to time.Time) ([]model.DataPoint, error) { + const autoSteps = 120 // Aim to have at least 120 data points per range. + + windows, ok := r.cache.SLOSLIWindows[sloID] + if !ok || len(windows) == 0 { + // Most probably that does not exist yet. + r.logger.Warningf("Could not find SLI windows for SLO ID %q", sloID) + return nil, nil + } + + idealStep := to.Sub(from) / autoSteps + // Get the best SLI based on the range. + step := windows[0] + for _, window := range windows { + // If the next step is bigger than our ideal step, we found a candidate. + if window >= idealStep { + break + } + step = window + } + + metric := conventions.GetSLIErrorMetric(step) // Use shortest window. + + return r.getSLIAvailabilityInRange(ctx, sloID, from, to, step, metric) +} + +func (r *Repository) getSLIAvailabilityInRange(ctx context.Context, sloID string, from, to time.Time, step time.Duration, sliMetric string) ([]model.DataPoint, error) { + query := fmt.Sprintf(`1 - (max(%s{sloth_id="%s"}))`, sliMetric, sloID) + r.logger.Debugf("Querying Prometheus with query=%q, from=%s, to=%s, step=%s", query, from, to, step) + + result, warnings, err := r.promcli.QueryRange(ctx, query, prometheusv1.Range{ + Start: from, + End: to, + Step: step, + }) + if err != nil { + return nil, fmt.Errorf("could not query prometheus: %w", err) + } + + for _, warning := range warnings { + r.logger.Warningf("Prometheus query warning: %v", warning) + } + + matrix, ok := result.(prommodel.Matrix) + if !ok { + return nil, fmt.Errorf("unexpected result type: %T", result) + } + + points := []model.DataPoint{} + for _, stream := range matrix { + for _, v := range stream.Values { + points = append(points, model.DataPoint{ + TS: v.Timestamp.Time().UTC(), + Value: float64(v.Value) * 100, + }) + } + } + + return points, nil +} diff --git a/internal/http/backend/storage/prometheus/prometheus_test.go b/internal/http/backend/storage/prometheus/prometheus_test.go new file mode 100644 index 00000000..4df5aa0c --- /dev/null +++ b/internal/http/backend/storage/prometheus/prometheus_test.go @@ -0,0 +1,784 @@ +package prometheus_test + +import ( + "fmt" + "testing" + "time" + + prometheusv1 "github.com/prometheus/client_golang/api/prometheus/v1" + prommodel "github.com/prometheus/common/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/slok/sloth/internal/http/backend/model" + "github.com/slok/sloth/internal/http/backend/storage" + "github.com/slok/sloth/internal/http/backend/storage/prometheus" + "github.com/slok/sloth/internal/http/backend/storage/prometheus/prometheusmock" +) + +func TestRepositoryListAllServiceAndAlerts(t *testing.T) { + tests := map[string]struct { + mock func(mpc *prometheusmock.PrometheusAPIClient) + expSvcAls []storage.ServiceAndAlerts + expErr bool + }{ + "Having errors while retrieving SLO details should fail.": { + mock: func(mpc *prometheusmock.PrometheusAPIClient) { + mpc.On("Query", mock.Anything, `max(slo:time_period:days{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + mpc.On("Query", mock.Anything, `max(sloth_slo_info{sloth_id!=""}) by (sloth_service, sloth_id, sloth_objective, sloth_slo)`, mock.Anything).Once().Return(nil, nil, fmt.Errorf("something")) + }, + expErr: true, + }, + + "Having errors while retrieving alerts details should fail.": { + mock: func(mpc *prometheusmock.PrometheusAPIClient) { + mpc.On("Query", mock.Anything, `max(slo:time_period:days{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + mpc.On("Query", mock.Anything, `max(sloth_slo_info{sloth_id!=""}) by (sloth_service, sloth_id, sloth_objective, sloth_slo)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + mpc.On("Query", mock.Anything, `max(ALERTS{sloth_id!=""}) by (alertname, sloth_id, alertstate, sloth_severity)`, mock.Anything).Once().Return(nil, nil, fmt.Errorf("something")) + }, + expErr: true, + }, + + "Getting SLOs and alerts successfully should return proper service and alerts.": { + mock: func(mpc *prometheusmock.PrometheusAPIClient) { + mpc.On("Query", mock.Anything, `max(slo:time_period:days{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 30}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 15}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 7}, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(sloth_slo_info{sloth_id!=""}) by (sloth_service, sloth_id, sloth_objective, sloth_slo)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-1", + "sloth_service": "svc-1", + "sloth_slo": "SLO 1", + "sloth_objective": "99.9", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-2", + "sloth_service": "svc-2", + "sloth_slo": "SLO 2", + "sloth_objective": "99.5", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-3", + "sloth_service": "svc-1", + "sloth_slo": "SLO 3", + "sloth_objective": "99.5", + }, + }, + }, nil, nil) + + mpc.On("Query", mock.Anything, `max(ALERTS{sloth_id!=""}) by (alertname, sloth_id, alertstate, sloth_severity)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{ + Metric: prommodel.Metric{ + "alertname": "warn-1", + "sloth_id": "slo-1", + "alertstate": "firing", + "sloth_service": "svc-1", + "sloth_severity": "ticket", + "sloth_slo": "SLO 1", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "alertname": "page-1", + "sloth_id": "slo-1", + "alertstate": "firing", + "sloth_service": "svc-1", + "sloth_severity": "page", + "sloth_slo": "SLO 1", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "alertname": "warn-2", + "sloth_id": "slo-2", + "alertstate": "firing", + "sloth_service": "svc-2", + "sloth_severity": "ticket", + "sloth_slo": "SLO 2", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "alertname": "page-3", + "sloth_id": "slo-3", + "alertstate": "firing", + "sloth_service": "svc-1", + "sloth_severity": "page", + "sloth_slo": "SLO 3", + }, + }, + }, nil, nil) + + mpc.On("Query", mock.Anything, `max(slo:period_error_budget_remaining:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + mpc.On("Query", mock.Anything, `max(slo:current_burn_rate:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + mpc.On("Query", mock.Anything, `count({__name__=~"^slo:sli_error:ratio_rate.*"}) by (__name__, sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + }, + expSvcAls: []storage.ServiceAndAlerts{ + { + Service: model.Service{ID: "svc-1"}, + Alerts: []model.SLOAlerts{ + { + SLOID: "slo-1", + FiringWarning: &model.Alert{Name: "warn-1"}, + FiringPage: &model.Alert{Name: "page-1"}, + }, + { + SLOID: "slo-3", + FiringPage: &model.Alert{Name: "page-3"}, + }, + }, + }, + { + Service: model.Service{ID: "svc-2"}, + Alerts: []model.SLOAlerts{ + { + SLOID: "slo-2", + FiringWarning: &model.Alert{Name: "warn-2"}, + }, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + mpc := prometheusmock.NewPrometheusAPIClient(t) + test.mock(mpc) + + repo, err := prometheus.NewRepository(t.Context(), prometheus.RepositoryConfig{ + PrometheusClient: mpc, + }) + + if test.expErr { + assert.Error(err) + return + } + assert.NoError(err) + + gotSvcAls, err := repo.ListAllServiceAndAlerts(t.Context()) + require.NoError(err) // Cache is populated on repo creation, thats where we test this. + assert.Equal(test.expSvcAls, gotSvcAls) + }) + } +} + +func TestRepositoryListSLOInstantDetailsService(t *testing.T) { + tests := map[string]struct { + mock func(mpc *prometheusmock.PrometheusAPIClient) + svcID string + expSLODet []storage.SLOInstantDetails + expErr bool + }{ + "Getting the list of SLO instant details from a specific service, should return the correct details.": { + svcID: "svc-1", + mock: func(mpc *prometheusmock.PrometheusAPIClient) { + mpc.On("Query", mock.Anything, `max(slo:time_period:days{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 30}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 15}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 15}, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(sloth_slo_info{sloth_id!=""}) by (sloth_service, sloth_id, sloth_objective, sloth_slo)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-1", + "sloth_service": "svc-1", + "sloth_slo": "SLO 1", + "sloth_objective": "99.9", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-2", + "sloth_service": "svc-1", + "sloth_slo": "SLO 2", + "sloth_objective": "99.5", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-3", + "sloth_service": "svc-2", + "sloth_slo": "SLO 3", + "sloth_objective": "99.5", + }, + }, + }, nil, nil) + + mpc.On("Query", mock.Anything, `max(ALERTS{sloth_id!=""}) by (alertname, sloth_id, alertstate, sloth_severity)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{ + Metric: prommodel.Metric{ + "alertname": "warn-1", + "sloth_id": "slo-1", + "alertstate": "firing", + "sloth_service": "svc-1", + "sloth_severity": "ticket", + "sloth_slo": "SLO 1", + }, + }, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(slo:period_error_budget_remaining:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 0.5}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 0.98}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 0.75}, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(slo:current_burn_rate:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 1}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 0.03}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 0.5}, + }, nil, nil) + + mpc.On("Query", mock.Anything, `count({__name__=~"^slo:sli_error:ratio_rate.*"}) by (__name__, sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + }, + expSLODet: []storage.SLOInstantDetails{ + { + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + PeriodDuration: 30 * 24 * time.Hour, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 100.0, + BurnedBudgetWindowPercent: 50.0, + }, + Alerts: model.SLOAlerts{ + SLOID: "slo-1", + FiringWarning: &model.Alert{Name: "warn-1"}, + }, + }, + { + SLO: model.SLO{ + ID: "slo-2", + Name: "SLO 2", + ServiceID: "svc-1", + Objective: 99.5, + PeriodDuration: 15 * 24 * time.Hour, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-2", + BurningBudgetPercent: 3.0, + BurnedBudgetWindowPercent: 2.0000000000000018, + }, + Alerts: model.SLOAlerts{}, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + mpc := prometheusmock.NewPrometheusAPIClient(t) + test.mock(mpc) + + repo, err := prometheus.NewRepository(t.Context(), prometheus.RepositoryConfig{ + PrometheusClient: mpc, + }) + + if test.expErr { + assert.Error(err) + return + } + assert.NoError(err) + + gotResult, err := repo.ListSLOInstantDetailsService(t.Context(), test.svcID) + require.NoError(err) // Cache is populated on repo creation, thats where we test this. + assert.Equal(test.expSLODet, gotResult) + }) + } +} + +func TestRepositoryListSLOInstantDetails(t *testing.T) { + tests := map[string]struct { + mock func(mpc *prometheusmock.PrometheusAPIClient) + expSLODet []storage.SLOInstantDetails + expErr bool + }{ + "Getting the list of SLO instant details, should return the correct details.": { + mock: func(mpc *prometheusmock.PrometheusAPIClient) { + mpc.On("Query", mock.Anything, `max(slo:time_period:days{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 30}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 15}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 15}, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(sloth_slo_info{sloth_id!=""}) by (sloth_service, sloth_id, sloth_objective, sloth_slo)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-1", + "sloth_service": "svc-1", + "sloth_slo": "SLO 1", + "sloth_objective": "99.9", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-2", + "sloth_service": "svc-1", + "sloth_slo": "SLO 2", + "sloth_objective": "99.5", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-3", + "sloth_service": "svc-2", + "sloth_slo": "SLO 3", + "sloth_objective": "99.5", + }, + }, + }, nil, nil) + + mpc.On("Query", mock.Anything, `max(ALERTS{sloth_id!=""}) by (alertname, sloth_id, alertstate, sloth_severity)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{ + Metric: prommodel.Metric{ + "alertname": "warn-1", + "sloth_id": "slo-1", + "alertstate": "firing", + "sloth_service": "svc-1", + "sloth_severity": "ticket", + "sloth_slo": "SLO 1", + }, + }, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(slo:period_error_budget_remaining:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 0.5}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 0.98}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 0.75}, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(slo:current_burn_rate:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 1}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 0.03}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 0.5}, + }, nil, nil) + + mpc.On("Query", mock.Anything, `count({__name__=~"^slo:sli_error:ratio_rate.*"}) by (__name__, sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + }, + expSLODet: []storage.SLOInstantDetails{ + { + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + PeriodDuration: 30 * 24 * time.Hour, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 100.0, + BurnedBudgetWindowPercent: 50.0, + }, + Alerts: model.SLOAlerts{ + SLOID: "slo-1", + FiringWarning: &model.Alert{Name: "warn-1"}, + }, + }, + { + SLO: model.SLO{ + ID: "slo-2", + Name: "SLO 2", + ServiceID: "svc-1", + Objective: 99.5, + PeriodDuration: 15 * 24 * time.Hour, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-2", + BurningBudgetPercent: 3.0, + BurnedBudgetWindowPercent: 2.0000000000000018, + }, + Alerts: model.SLOAlerts{}, + }, + { + SLO: model.SLO{ + ID: "slo-3", + Name: "SLO 3", + ServiceID: "svc-2", + Objective: 99.5, + PeriodDuration: 15 * 24 * time.Hour, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-3", + BurningBudgetPercent: 50.0, + BurnedBudgetWindowPercent: 25.0, + }, + Alerts: model.SLOAlerts{}, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + mpc := prometheusmock.NewPrometheusAPIClient(t) + test.mock(mpc) + + repo, err := prometheus.NewRepository(t.Context(), prometheus.RepositoryConfig{ + PrometheusClient: mpc, + }) + + if test.expErr { + assert.Error(err) + return + } + assert.NoError(err) + + gotResult, err := repo.ListSLOInstantDetails(t.Context()) + require.NoError(err) // Cache is populated on repo creation, thats where we test this. + assert.Equal(test.expSLODet, gotResult) + }) + } +} + +func TestRepositoryGetSLOInstantDetails(t *testing.T) { + tests := map[string]struct { + mock func(mpc *prometheusmock.PrometheusAPIClient) + sloID string + expSLODet storage.SLOInstantDetails + expErr bool + }{ + "Getting an SLO instant details, should return the correct details.": { + sloID: "slo-1", + mock: func(mpc *prometheusmock.PrometheusAPIClient) { + mpc.On("Query", mock.Anything, `max(slo:time_period:days{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 30}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 15}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 15}, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(sloth_slo_info{sloth_id!=""}) by (sloth_service, sloth_id, sloth_objective, sloth_slo)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-1", + "sloth_service": "svc-1", + "sloth_slo": "SLO 1", + "sloth_objective": "99.9", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-2", + "sloth_service": "svc-1", + "sloth_slo": "SLO 2", + "sloth_objective": "99.5", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-3", + "sloth_service": "svc-2", + "sloth_slo": "SLO 3", + "sloth_objective": "99.5", + }, + }, + }, nil, nil) + + mpc.On("Query", mock.Anything, `max(ALERTS{sloth_id!=""}) by (alertname, sloth_id, alertstate, sloth_severity)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{ + Metric: prommodel.Metric{ + "alertname": "warn-1", + "sloth_id": "slo-1", + "alertstate": "firing", + "sloth_service": "svc-1", + "sloth_severity": "ticket", + "sloth_slo": "SLO 1", + }, + }, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(slo:period_error_budget_remaining:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 0.5}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 0.98}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 0.75}, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(slo:current_burn_rate:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 1}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 0.03}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 0.5}, + }, nil, nil) + + mpc.On("Query", mock.Anything, `count({__name__=~"^slo:sli_error:ratio_rate.*"}) by (__name__, sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + }, + expSLODet: storage.SLOInstantDetails{ + SLO: model.SLO{ + ID: "slo-1", + Name: "SLO 1", + ServiceID: "svc-1", + Objective: 99.9, + PeriodDuration: 30 * 24 * time.Hour, + }, + BudgetDetails: model.SLOBudgetDetails{ + SLOID: "slo-1", + BurningBudgetPercent: 100.0, + BurnedBudgetWindowPercent: 50.0, + }, + Alerts: model.SLOAlerts{ + SLOID: "slo-1", + FiringWarning: &model.Alert{Name: "warn-1"}, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + mpc := prometheusmock.NewPrometheusAPIClient(t) + test.mock(mpc) + + repo, err := prometheus.NewRepository(t.Context(), prometheus.RepositoryConfig{ + PrometheusClient: mpc, + }) + + if test.expErr { + assert.Error(err) + return + } + assert.NoError(err) + + gotResult, err := repo.GetSLOInstantDetails(t.Context(), test.sloID) + require.NoError(err) // Cache is populated on repo creation, thats where we test this. + assert.Equal(test.expSLODet, *gotResult) + }) + } +} + +func TestRepositoryGetSLIAvailabilityInRange(t *testing.T) { + t0, _ := time.Parse(time.RFC3339, "2025-11-16T01:02:03Z") + + tests := map[string]struct { + mock func(mpc *prometheusmock.PrometheusAPIClient) + sloID string + from time.Time + to time.Time + step time.Duration + expDPs []model.DataPoint + expErr bool + }{ + "Getting an SLO instant details, should return the correct details.": { + sloID: "slo-1", + from: t0, + to: t0.Add(1 * time.Hour), + step: 15 * time.Minute, + mock: func(mpc *prometheusmock.PrometheusAPIClient) { + mpc.On("Query", mock.Anything, `max(slo:time_period:days{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 30}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 15}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 15}, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(sloth_slo_info{sloth_id!=""}) by (sloth_service, sloth_id, sloth_objective, sloth_slo)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-1", + "sloth_service": "svc-1", + "sloth_slo": "SLO 1", + "sloth_objective": "99.9", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-2", + "sloth_service": "svc-1", + "sloth_slo": "SLO 2", + "sloth_objective": "99.5", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-3", + "sloth_service": "svc-2", + "sloth_slo": "SLO 3", + "sloth_objective": "99.5", + }, + }, + }, nil, nil) + + mpc.On("Query", mock.Anything, `max(ALERTS{sloth_id!=""}) by (alertname, sloth_id, alertstate, sloth_severity)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + mpc.On("Query", mock.Anything, `max(slo:period_error_budget_remaining:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + mpc.On("Query", mock.Anything, `max(slo:current_burn_rate:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + mpc.On("Query", mock.Anything, `count({__name__=~"^slo:sli_error:ratio_rate.*"}) by (__name__, sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"__name__": "slo:sli_error:ratio_rate42m", "sloth_id": "slo-1"}, Value: 0}, + &prommodel.Sample{Metric: prommodel.Metric{"__name__": "slo:sli_error:ratio_rate31m", "sloth_id": "slo-1"}, Value: 0}, // This is the short window required to infer. + &prommodel.Sample{Metric: prommodel.Metric{"__name__": "slo:sli_error:ratio_rate5m", "sloth_id": "slo-2"}, Value: 0}, + }, nil, nil) + expRange := prometheusv1.Range{ + Start: t0, + End: t0.Add(1 * time.Hour), + Step: 15 * time.Minute, + } + mpc.On("QueryRange", mock.Anything, `1 - (max(slo:sli_error:ratio_rate31m{sloth_id="slo-1"}))`, expRange).Once().Return(prommodel.Matrix{ + &prommodel.SampleStream{ + Metric: prommodel.Metric{"sloth_id": "slo-1"}, + Values: []prommodel.SamplePair{ + {Timestamp: prommodel.TimeFromUnix(t0.UTC().Unix()), Value: 1}, + {Timestamp: prommodel.TimeFromUnix(t0.UTC().Add(15 * time.Minute).Unix()), Value: 2}, + {Timestamp: prommodel.TimeFromUnix(t0.UTC().Add(30 * time.Minute).Unix()), Value: 3}, + {Timestamp: prommodel.TimeFromUnix(t0.UTC().Add(45 * time.Minute).Unix()), Value: 4}, + {Timestamp: prommodel.TimeFromUnix(t0.UTC().Add(60 * time.Minute).Unix()), Value: 5}, + }, + }, + }, nil, nil) + }, + expDPs: []model.DataPoint{ + {TS: t0.UTC(), Value: 100}, + {TS: t0.UTC().Add(15 * time.Minute), Value: 200}, + {TS: t0.UTC().Add(30 * time.Minute), Value: 300}, + {TS: t0.UTC().Add(45 * time.Minute), Value: 400}, + {TS: t0.UTC().Add(60 * time.Minute), Value: 500}, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + mpc := prometheusmock.NewPrometheusAPIClient(t) + test.mock(mpc) + + repo, err := prometheus.NewRepository(t.Context(), prometheus.RepositoryConfig{ + PrometheusClient: mpc, + }) + + if test.expErr { + assert.Error(err) + return + } + assert.NoError(err) + + gotResult, err := repo.GetSLIAvailabilityInRange(t.Context(), test.sloID, test.from, test.to, test.step) + require.NoError(err) // Cache is populated on repo creation, thats where we test this. + assert.Equal(test.expDPs, gotResult) + }) + } +} + +func TestRepositoryGetSLIAvailabilityInRangeAutoStep(t *testing.T) { + t0, _ := time.Parse(time.RFC3339, "2025-11-16T01:02:03Z") + + tests := map[string]struct { + mock func(mpc *prometheusmock.PrometheusAPIClient) + sloID string + from time.Time + to time.Time + expDPs []model.DataPoint + expErr bool + }{ + "Getting an SLO instant details, should return the correct details.": { + sloID: "slo-1", + from: t0, + to: t0.Add(24 * time.Hour), + mock: func(mpc *prometheusmock.PrometheusAPIClient) { + mpc.On("Query", mock.Anything, `max(slo:time_period:days{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-1"}, Value: 30}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-2"}, Value: 15}, + &prommodel.Sample{Metric: prommodel.Metric{"sloth_id": "slo-3"}, Value: 15}, + }, nil, nil) + mpc.On("Query", mock.Anything, `max(sloth_slo_info{sloth_id!=""}) by (sloth_service, sloth_id, sloth_objective, sloth_slo)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-1", + "sloth_service": "svc-1", + "sloth_slo": "SLO 1", + "sloth_objective": "99.9", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-2", + "sloth_service": "svc-1", + "sloth_slo": "SLO 2", + "sloth_objective": "99.5", + }, + }, + &prommodel.Sample{ + Metric: prommodel.Metric{ + "sloth_id": "slo-3", + "sloth_service": "svc-2", + "sloth_slo": "SLO 3", + "sloth_objective": "99.5", + }, + }, + }, nil, nil) + + mpc.On("Query", mock.Anything, `max(ALERTS{sloth_id!=""}) by (alertname, sloth_id, alertstate, sloth_severity)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + mpc.On("Query", mock.Anything, `max(slo:period_error_budget_remaining:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + mpc.On("Query", mock.Anything, `max(slo:current_burn_rate:ratio{sloth_id!=""}) by (sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{}, nil, nil) + mpc.On("Query", mock.Anything, `count({__name__=~"^slo:sli_error:ratio_rate.*"}) by (__name__, sloth_id)`, mock.Anything).Once().Return(prommodel.Vector{ + &prommodel.Sample{Metric: prommodel.Metric{"__name__": "slo:sli_error:ratio_rate42m", "sloth_id": "slo-1"}, Value: 0}, + &prommodel.Sample{Metric: prommodel.Metric{"__name__": "slo:sli_error:ratio_rate31m", "sloth_id": "slo-1"}, Value: 0}, + &prommodel.Sample{Metric: prommodel.Metric{"__name__": "slo:sli_error:ratio_rate10m", "sloth_id": "slo-1"}, Value: 0}, // Expected window with auto step. + &prommodel.Sample{Metric: prommodel.Metric{"__name__": "slo:sli_error:ratio_rate5m", "sloth_id": "slo-1"}, Value: 0}, + &prommodel.Sample{Metric: prommodel.Metric{"__name__": "slo:sli_error:ratio_rate1m", "sloth_id": "slo-1"}, Value: 0}, + &prommodel.Sample{Metric: prommodel.Metric{"__name__": "slo:sli_error:ratio_rate5m", "sloth_id": "slo-2"}, Value: 0}, + }, nil, nil) + expRange := prometheusv1.Range{ + Start: t0, + End: t0.Add(24 * time.Hour), + Step: 10 * time.Minute, + } + mpc.On("QueryRange", mock.Anything, `1 - (max(slo:sli_error:ratio_rate10m{sloth_id="slo-1"}))`, expRange).Once().Return(prommodel.Matrix{ + &prommodel.SampleStream{ + Metric: prommodel.Metric{"sloth_id": "slo-1"}, + Values: []prommodel.SamplePair{ + {Timestamp: prommodel.TimeFromUnix(t0.UTC().Unix()), Value: 1}, + {Timestamp: prommodel.TimeFromUnix(t0.UTC().Add(15 * time.Minute).Unix()), Value: 2}, + {Timestamp: prommodel.TimeFromUnix(t0.UTC().Add(30 * time.Minute).Unix()), Value: 3}, + {Timestamp: prommodel.TimeFromUnix(t0.UTC().Add(45 * time.Minute).Unix()), Value: 4}, + {Timestamp: prommodel.TimeFromUnix(t0.UTC().Add(60 * time.Minute).Unix()), Value: 5}, + }, + }, + }, nil, nil) + }, + expDPs: []model.DataPoint{ + {TS: t0.UTC(), Value: 100}, + {TS: t0.UTC().Add(15 * time.Minute), Value: 200}, + {TS: t0.UTC().Add(30 * time.Minute), Value: 300}, + {TS: t0.UTC().Add(45 * time.Minute), Value: 400}, + {TS: t0.UTC().Add(60 * time.Minute), Value: 500}, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + mpc := prometheusmock.NewPrometheusAPIClient(t) + test.mock(mpc) + + repo, err := prometheus.NewRepository(t.Context(), prometheus.RepositoryConfig{ + PrometheusClient: mpc, + }) + + if test.expErr { + assert.Error(err) + return + } + assert.NoError(err) + + gotResult, err := repo.GetSLIAvailabilityInRangeAutoStep(t.Context(), test.sloID, test.from, test.to) + require.NoError(err) // Cache is populated on repo creation, thats where we test this. + assert.Equal(test.expDPs, gotResult) + }) + } +} diff --git a/internal/http/backend/storage/prometheus/prometheusmock/mocks.go b/internal/http/backend/storage/prometheus/prometheusmock/mocks.go new file mode 100644 index 00000000..1e7247aa --- /dev/null +++ b/internal/http/backend/storage/prometheus/prometheusmock/mocks.go @@ -0,0 +1,1587 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package prometheusmock + +import ( + "context" + "time" + + "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/prometheus/common/model" + mock "github.com/stretchr/testify/mock" +) + +// NewPrometheusAPIClient creates a new instance of PrometheusAPIClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPrometheusAPIClient(t interface { + mock.TestingT + Cleanup(func()) +}) *PrometheusAPIClient { + mock := &PrometheusAPIClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// PrometheusAPIClient is an autogenerated mock type for the PrometheusAPIClient type +type PrometheusAPIClient struct { + mock.Mock +} + +type PrometheusAPIClient_Expecter struct { + mock *mock.Mock +} + +func (_m *PrometheusAPIClient) EXPECT() *PrometheusAPIClient_Expecter { + return &PrometheusAPIClient_Expecter{mock: &_m.Mock} +} + +// AlertManagers provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) AlertManagers(ctx context.Context) (v1.AlertManagersResult, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for AlertManagers") + } + + var r0 v1.AlertManagersResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) (v1.AlertManagersResult, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) v1.AlertManagersResult); ok { + r0 = returnFunc(ctx) + } else { + r0 = ret.Get(0).(v1.AlertManagersResult) + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_AlertManagers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AlertManagers' +type PrometheusAPIClient_AlertManagers_Call struct { + *mock.Call +} + +// AlertManagers is a helper method to define mock.On call +// - ctx context.Context +func (_e *PrometheusAPIClient_Expecter) AlertManagers(ctx interface{}) *PrometheusAPIClient_AlertManagers_Call { + return &PrometheusAPIClient_AlertManagers_Call{Call: _e.mock.On("AlertManagers", ctx)} +} + +func (_c *PrometheusAPIClient_AlertManagers_Call) Run(run func(ctx context.Context)) *PrometheusAPIClient_AlertManagers_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_AlertManagers_Call) Return(alertManagersResult v1.AlertManagersResult, err error) *PrometheusAPIClient_AlertManagers_Call { + _c.Call.Return(alertManagersResult, err) + return _c +} + +func (_c *PrometheusAPIClient_AlertManagers_Call) RunAndReturn(run func(ctx context.Context) (v1.AlertManagersResult, error)) *PrometheusAPIClient_AlertManagers_Call { + _c.Call.Return(run) + return _c +} + +// Alerts provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) Alerts(ctx context.Context) (v1.AlertsResult, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Alerts") + } + + var r0 v1.AlertsResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) (v1.AlertsResult, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) v1.AlertsResult); ok { + r0 = returnFunc(ctx) + } else { + r0 = ret.Get(0).(v1.AlertsResult) + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_Alerts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Alerts' +type PrometheusAPIClient_Alerts_Call struct { + *mock.Call +} + +// Alerts is a helper method to define mock.On call +// - ctx context.Context +func (_e *PrometheusAPIClient_Expecter) Alerts(ctx interface{}) *PrometheusAPIClient_Alerts_Call { + return &PrometheusAPIClient_Alerts_Call{Call: _e.mock.On("Alerts", ctx)} +} + +func (_c *PrometheusAPIClient_Alerts_Call) Run(run func(ctx context.Context)) *PrometheusAPIClient_Alerts_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_Alerts_Call) Return(alertsResult v1.AlertsResult, err error) *PrometheusAPIClient_Alerts_Call { + _c.Call.Return(alertsResult, err) + return _c +} + +func (_c *PrometheusAPIClient_Alerts_Call) RunAndReturn(run func(ctx context.Context) (v1.AlertsResult, error)) *PrometheusAPIClient_Alerts_Call { + _c.Call.Return(run) + return _c +} + +// Buildinfo provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) Buildinfo(ctx context.Context) (v1.BuildinfoResult, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Buildinfo") + } + + var r0 v1.BuildinfoResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) (v1.BuildinfoResult, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) v1.BuildinfoResult); ok { + r0 = returnFunc(ctx) + } else { + r0 = ret.Get(0).(v1.BuildinfoResult) + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_Buildinfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Buildinfo' +type PrometheusAPIClient_Buildinfo_Call struct { + *mock.Call +} + +// Buildinfo is a helper method to define mock.On call +// - ctx context.Context +func (_e *PrometheusAPIClient_Expecter) Buildinfo(ctx interface{}) *PrometheusAPIClient_Buildinfo_Call { + return &PrometheusAPIClient_Buildinfo_Call{Call: _e.mock.On("Buildinfo", ctx)} +} + +func (_c *PrometheusAPIClient_Buildinfo_Call) Run(run func(ctx context.Context)) *PrometheusAPIClient_Buildinfo_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_Buildinfo_Call) Return(buildinfoResult v1.BuildinfoResult, err error) *PrometheusAPIClient_Buildinfo_Call { + _c.Call.Return(buildinfoResult, err) + return _c +} + +func (_c *PrometheusAPIClient_Buildinfo_Call) RunAndReturn(run func(ctx context.Context) (v1.BuildinfoResult, error)) *PrometheusAPIClient_Buildinfo_Call { + _c.Call.Return(run) + return _c +} + +// CleanTombstones provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) CleanTombstones(ctx context.Context) error { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for CleanTombstones") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = returnFunc(ctx) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// PrometheusAPIClient_CleanTombstones_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CleanTombstones' +type PrometheusAPIClient_CleanTombstones_Call struct { + *mock.Call +} + +// CleanTombstones is a helper method to define mock.On call +// - ctx context.Context +func (_e *PrometheusAPIClient_Expecter) CleanTombstones(ctx interface{}) *PrometheusAPIClient_CleanTombstones_Call { + return &PrometheusAPIClient_CleanTombstones_Call{Call: _e.mock.On("CleanTombstones", ctx)} +} + +func (_c *PrometheusAPIClient_CleanTombstones_Call) Run(run func(ctx context.Context)) *PrometheusAPIClient_CleanTombstones_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_CleanTombstones_Call) Return(err error) *PrometheusAPIClient_CleanTombstones_Call { + _c.Call.Return(err) + return _c +} + +func (_c *PrometheusAPIClient_CleanTombstones_Call) RunAndReturn(run func(ctx context.Context) error) *PrometheusAPIClient_CleanTombstones_Call { + _c.Call.Return(run) + return _c +} + +// Config provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) Config(ctx context.Context) (v1.ConfigResult, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Config") + } + + var r0 v1.ConfigResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) (v1.ConfigResult, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) v1.ConfigResult); ok { + r0 = returnFunc(ctx) + } else { + r0 = ret.Get(0).(v1.ConfigResult) + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_Config_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Config' +type PrometheusAPIClient_Config_Call struct { + *mock.Call +} + +// Config is a helper method to define mock.On call +// - ctx context.Context +func (_e *PrometheusAPIClient_Expecter) Config(ctx interface{}) *PrometheusAPIClient_Config_Call { + return &PrometheusAPIClient_Config_Call{Call: _e.mock.On("Config", ctx)} +} + +func (_c *PrometheusAPIClient_Config_Call) Run(run func(ctx context.Context)) *PrometheusAPIClient_Config_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_Config_Call) Return(configResult v1.ConfigResult, err error) *PrometheusAPIClient_Config_Call { + _c.Call.Return(configResult, err) + return _c +} + +func (_c *PrometheusAPIClient_Config_Call) RunAndReturn(run func(ctx context.Context) (v1.ConfigResult, error)) *PrometheusAPIClient_Config_Call { + _c.Call.Return(run) + return _c +} + +// DeleteSeries provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) DeleteSeries(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error { + ret := _mock.Called(ctx, matches, startTime, endTime) + + if len(ret) == 0 { + panic("no return value specified for DeleteSeries") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, []string, time.Time, time.Time) error); ok { + r0 = returnFunc(ctx, matches, startTime, endTime) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// PrometheusAPIClient_DeleteSeries_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteSeries' +type PrometheusAPIClient_DeleteSeries_Call struct { + *mock.Call +} + +// DeleteSeries is a helper method to define mock.On call +// - ctx context.Context +// - matches []string +// - startTime time.Time +// - endTime time.Time +func (_e *PrometheusAPIClient_Expecter) DeleteSeries(ctx interface{}, matches interface{}, startTime interface{}, endTime interface{}) *PrometheusAPIClient_DeleteSeries_Call { + return &PrometheusAPIClient_DeleteSeries_Call{Call: _e.mock.On("DeleteSeries", ctx, matches, startTime, endTime)} +} + +func (_c *PrometheusAPIClient_DeleteSeries_Call) Run(run func(ctx context.Context, matches []string, startTime time.Time, endTime time.Time)) *PrometheusAPIClient_DeleteSeries_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + if args[1] != nil { + arg1 = args[1].([]string) + } + var arg2 time.Time + if args[2] != nil { + arg2 = args[2].(time.Time) + } + var arg3 time.Time + if args[3] != nil { + arg3 = args[3].(time.Time) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_DeleteSeries_Call) Return(err error) *PrometheusAPIClient_DeleteSeries_Call { + _c.Call.Return(err) + return _c +} + +func (_c *PrometheusAPIClient_DeleteSeries_Call) RunAndReturn(run func(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) error) *PrometheusAPIClient_DeleteSeries_Call { + _c.Call.Return(run) + return _c +} + +// Flags provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) Flags(ctx context.Context) (v1.FlagsResult, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Flags") + } + + var r0 v1.FlagsResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) (v1.FlagsResult, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) v1.FlagsResult); ok { + r0 = returnFunc(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(v1.FlagsResult) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_Flags_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Flags' +type PrometheusAPIClient_Flags_Call struct { + *mock.Call +} + +// Flags is a helper method to define mock.On call +// - ctx context.Context +func (_e *PrometheusAPIClient_Expecter) Flags(ctx interface{}) *PrometheusAPIClient_Flags_Call { + return &PrometheusAPIClient_Flags_Call{Call: _e.mock.On("Flags", ctx)} +} + +func (_c *PrometheusAPIClient_Flags_Call) Run(run func(ctx context.Context)) *PrometheusAPIClient_Flags_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_Flags_Call) Return(flagsResult v1.FlagsResult, err error) *PrometheusAPIClient_Flags_Call { + _c.Call.Return(flagsResult, err) + return _c +} + +func (_c *PrometheusAPIClient_Flags_Call) RunAndReturn(run func(ctx context.Context) (v1.FlagsResult, error)) *PrometheusAPIClient_Flags_Call { + _c.Call.Return(run) + return _c +} + +// LabelNames provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) LabelNames(ctx context.Context, matches []string, startTime time.Time, endTime time.Time, opts ...v1.Option) ([]string, v1.Warnings, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, matches, startTime, endTime, opts) + } else { + tmpRet = _mock.Called(ctx, matches, startTime, endTime) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for LabelNames") + } + + var r0 []string + var r1 v1.Warnings + var r2 error + if returnFunc, ok := ret.Get(0).(func(context.Context, []string, time.Time, time.Time, ...v1.Option) ([]string, v1.Warnings, error)); ok { + return returnFunc(ctx, matches, startTime, endTime, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, []string, time.Time, time.Time, ...v1.Option) []string); ok { + r0 = returnFunc(ctx, matches, startTime, endTime, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, []string, time.Time, time.Time, ...v1.Option) v1.Warnings); ok { + r1 = returnFunc(ctx, matches, startTime, endTime, opts...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(v1.Warnings) + } + } + if returnFunc, ok := ret.Get(2).(func(context.Context, []string, time.Time, time.Time, ...v1.Option) error); ok { + r2 = returnFunc(ctx, matches, startTime, endTime, opts...) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// PrometheusAPIClient_LabelNames_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LabelNames' +type PrometheusAPIClient_LabelNames_Call struct { + *mock.Call +} + +// LabelNames is a helper method to define mock.On call +// - ctx context.Context +// - matches []string +// - startTime time.Time +// - endTime time.Time +// - opts ...v1.Option +func (_e *PrometheusAPIClient_Expecter) LabelNames(ctx interface{}, matches interface{}, startTime interface{}, endTime interface{}, opts ...interface{}) *PrometheusAPIClient_LabelNames_Call { + return &PrometheusAPIClient_LabelNames_Call{Call: _e.mock.On("LabelNames", + append([]interface{}{ctx, matches, startTime, endTime}, opts...)...)} +} + +func (_c *PrometheusAPIClient_LabelNames_Call) Run(run func(ctx context.Context, matches []string, startTime time.Time, endTime time.Time, opts ...v1.Option)) *PrometheusAPIClient_LabelNames_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + if args[1] != nil { + arg1 = args[1].([]string) + } + var arg2 time.Time + if args[2] != nil { + arg2 = args[2].(time.Time) + } + var arg3 time.Time + if args[3] != nil { + arg3 = args[3].(time.Time) + } + var arg4 []v1.Option + var variadicArgs []v1.Option + if len(args) > 4 { + variadicArgs = args[4].([]v1.Option) + } + arg4 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3, + arg4..., + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_LabelNames_Call) Return(strings []string, warnings v1.Warnings, err error) *PrometheusAPIClient_LabelNames_Call { + _c.Call.Return(strings, warnings, err) + return _c +} + +func (_c *PrometheusAPIClient_LabelNames_Call) RunAndReturn(run func(ctx context.Context, matches []string, startTime time.Time, endTime time.Time, opts ...v1.Option) ([]string, v1.Warnings, error)) *PrometheusAPIClient_LabelNames_Call { + _c.Call.Return(run) + return _c +} + +// LabelValues provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) LabelValues(ctx context.Context, label string, matches []string, startTime time.Time, endTime time.Time, opts ...v1.Option) (model.LabelValues, v1.Warnings, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, label, matches, startTime, endTime, opts) + } else { + tmpRet = _mock.Called(ctx, label, matches, startTime, endTime) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for LabelValues") + } + + var r0 model.LabelValues + var r1 v1.Warnings + var r2 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, time.Time, time.Time, ...v1.Option) (model.LabelValues, v1.Warnings, error)); ok { + return returnFunc(ctx, label, matches, startTime, endTime, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, time.Time, time.Time, ...v1.Option) model.LabelValues); ok { + r0 = returnFunc(ctx, label, matches, startTime, endTime, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(model.LabelValues) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, []string, time.Time, time.Time, ...v1.Option) v1.Warnings); ok { + r1 = returnFunc(ctx, label, matches, startTime, endTime, opts...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(v1.Warnings) + } + } + if returnFunc, ok := ret.Get(2).(func(context.Context, string, []string, time.Time, time.Time, ...v1.Option) error); ok { + r2 = returnFunc(ctx, label, matches, startTime, endTime, opts...) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// PrometheusAPIClient_LabelValues_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LabelValues' +type PrometheusAPIClient_LabelValues_Call struct { + *mock.Call +} + +// LabelValues is a helper method to define mock.On call +// - ctx context.Context +// - label string +// - matches []string +// - startTime time.Time +// - endTime time.Time +// - opts ...v1.Option +func (_e *PrometheusAPIClient_Expecter) LabelValues(ctx interface{}, label interface{}, matches interface{}, startTime interface{}, endTime interface{}, opts ...interface{}) *PrometheusAPIClient_LabelValues_Call { + return &PrometheusAPIClient_LabelValues_Call{Call: _e.mock.On("LabelValues", + append([]interface{}{ctx, label, matches, startTime, endTime}, opts...)...)} +} + +func (_c *PrometheusAPIClient_LabelValues_Call) Run(run func(ctx context.Context, label string, matches []string, startTime time.Time, endTime time.Time, opts ...v1.Option)) *PrometheusAPIClient_LabelValues_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + var arg3 time.Time + if args[3] != nil { + arg3 = args[3].(time.Time) + } + var arg4 time.Time + if args[4] != nil { + arg4 = args[4].(time.Time) + } + var arg5 []v1.Option + var variadicArgs []v1.Option + if len(args) > 5 { + variadicArgs = args[5].([]v1.Option) + } + arg5 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3, + arg4, + arg5..., + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_LabelValues_Call) Return(labelValues model.LabelValues, warnings v1.Warnings, err error) *PrometheusAPIClient_LabelValues_Call { + _c.Call.Return(labelValues, warnings, err) + return _c +} + +func (_c *PrometheusAPIClient_LabelValues_Call) RunAndReturn(run func(ctx context.Context, label string, matches []string, startTime time.Time, endTime time.Time, opts ...v1.Option) (model.LabelValues, v1.Warnings, error)) *PrometheusAPIClient_LabelValues_Call { + _c.Call.Return(run) + return _c +} + +// Metadata provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) Metadata(ctx context.Context, metric string, limit string) (map[string][]v1.Metadata, error) { + ret := _mock.Called(ctx, metric, limit) + + if len(ret) == 0 { + panic("no return value specified for Metadata") + } + + var r0 map[string][]v1.Metadata + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (map[string][]v1.Metadata, error)); ok { + return returnFunc(ctx, metric, limit) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) map[string][]v1.Metadata); ok { + r0 = returnFunc(ctx, metric, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string][]v1.Metadata) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = returnFunc(ctx, metric, limit) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_Metadata_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Metadata' +type PrometheusAPIClient_Metadata_Call struct { + *mock.Call +} + +// Metadata is a helper method to define mock.On call +// - ctx context.Context +// - metric string +// - limit string +func (_e *PrometheusAPIClient_Expecter) Metadata(ctx interface{}, metric interface{}, limit interface{}) *PrometheusAPIClient_Metadata_Call { + return &PrometheusAPIClient_Metadata_Call{Call: _e.mock.On("Metadata", ctx, metric, limit)} +} + +func (_c *PrometheusAPIClient_Metadata_Call) Run(run func(ctx context.Context, metric string, limit string)) *PrometheusAPIClient_Metadata_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_Metadata_Call) Return(stringToMetadatas map[string][]v1.Metadata, err error) *PrometheusAPIClient_Metadata_Call { + _c.Call.Return(stringToMetadatas, err) + return _c +} + +func (_c *PrometheusAPIClient_Metadata_Call) RunAndReturn(run func(ctx context.Context, metric string, limit string) (map[string][]v1.Metadata, error)) *PrometheusAPIClient_Metadata_Call { + _c.Call.Return(run) + return _c +} + +// Query provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) Query(ctx context.Context, query string, ts time.Time, opts ...v1.Option) (model.Value, v1.Warnings, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, query, ts, opts) + } else { + tmpRet = _mock.Called(ctx, query, ts) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for Query") + } + + var r0 model.Value + var r1 v1.Warnings + var r2 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time, ...v1.Option) (model.Value, v1.Warnings, error)); ok { + return returnFunc(ctx, query, ts, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time, ...v1.Option) model.Value); ok { + r0 = returnFunc(ctx, query, ts, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(model.Value) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, time.Time, ...v1.Option) v1.Warnings); ok { + r1 = returnFunc(ctx, query, ts, opts...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(v1.Warnings) + } + } + if returnFunc, ok := ret.Get(2).(func(context.Context, string, time.Time, ...v1.Option) error); ok { + r2 = returnFunc(ctx, query, ts, opts...) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// PrometheusAPIClient_Query_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Query' +type PrometheusAPIClient_Query_Call struct { + *mock.Call +} + +// Query is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - ts time.Time +// - opts ...v1.Option +func (_e *PrometheusAPIClient_Expecter) Query(ctx interface{}, query interface{}, ts interface{}, opts ...interface{}) *PrometheusAPIClient_Query_Call { + return &PrometheusAPIClient_Query_Call{Call: _e.mock.On("Query", + append([]interface{}{ctx, query, ts}, opts...)...)} +} + +func (_c *PrometheusAPIClient_Query_Call) Run(run func(ctx context.Context, query string, ts time.Time, opts ...v1.Option)) *PrometheusAPIClient_Query_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 time.Time + if args[2] != nil { + arg2 = args[2].(time.Time) + } + var arg3 []v1.Option + var variadicArgs []v1.Option + if len(args) > 3 { + variadicArgs = args[3].([]v1.Option) + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_Query_Call) Return(value model.Value, warnings v1.Warnings, err error) *PrometheusAPIClient_Query_Call { + _c.Call.Return(value, warnings, err) + return _c +} + +func (_c *PrometheusAPIClient_Query_Call) RunAndReturn(run func(ctx context.Context, query string, ts time.Time, opts ...v1.Option) (model.Value, v1.Warnings, error)) *PrometheusAPIClient_Query_Call { + _c.Call.Return(run) + return _c +} + +// QueryExemplars provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) QueryExemplars(ctx context.Context, query string, startTime time.Time, endTime time.Time) ([]v1.ExemplarQueryResult, error) { + ret := _mock.Called(ctx, query, startTime, endTime) + + if len(ret) == 0 { + panic("no return value specified for QueryExemplars") + } + + var r0 []v1.ExemplarQueryResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time, time.Time) ([]v1.ExemplarQueryResult, error)); ok { + return returnFunc(ctx, query, startTime, endTime) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time, time.Time) []v1.ExemplarQueryResult); ok { + r0 = returnFunc(ctx, query, startTime, endTime) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]v1.ExemplarQueryResult) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, time.Time, time.Time) error); ok { + r1 = returnFunc(ctx, query, startTime, endTime) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_QueryExemplars_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryExemplars' +type PrometheusAPIClient_QueryExemplars_Call struct { + *mock.Call +} + +// QueryExemplars is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - startTime time.Time +// - endTime time.Time +func (_e *PrometheusAPIClient_Expecter) QueryExemplars(ctx interface{}, query interface{}, startTime interface{}, endTime interface{}) *PrometheusAPIClient_QueryExemplars_Call { + return &PrometheusAPIClient_QueryExemplars_Call{Call: _e.mock.On("QueryExemplars", ctx, query, startTime, endTime)} +} + +func (_c *PrometheusAPIClient_QueryExemplars_Call) Run(run func(ctx context.Context, query string, startTime time.Time, endTime time.Time)) *PrometheusAPIClient_QueryExemplars_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 time.Time + if args[2] != nil { + arg2 = args[2].(time.Time) + } + var arg3 time.Time + if args[3] != nil { + arg3 = args[3].(time.Time) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_QueryExemplars_Call) Return(exemplarQueryResults []v1.ExemplarQueryResult, err error) *PrometheusAPIClient_QueryExemplars_Call { + _c.Call.Return(exemplarQueryResults, err) + return _c +} + +func (_c *PrometheusAPIClient_QueryExemplars_Call) RunAndReturn(run func(ctx context.Context, query string, startTime time.Time, endTime time.Time) ([]v1.ExemplarQueryResult, error)) *PrometheusAPIClient_QueryExemplars_Call { + _c.Call.Return(run) + return _c +} + +// QueryRange provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) QueryRange(ctx context.Context, query string, r v1.Range, opts ...v1.Option) (model.Value, v1.Warnings, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, query, r, opts) + } else { + tmpRet = _mock.Called(ctx, query, r) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for QueryRange") + } + + var r0 model.Value + var r1 v1.Warnings + var r2 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, v1.Range, ...v1.Option) (model.Value, v1.Warnings, error)); ok { + return returnFunc(ctx, query, r, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, v1.Range, ...v1.Option) model.Value); ok { + r0 = returnFunc(ctx, query, r, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(model.Value) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, v1.Range, ...v1.Option) v1.Warnings); ok { + r1 = returnFunc(ctx, query, r, opts...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(v1.Warnings) + } + } + if returnFunc, ok := ret.Get(2).(func(context.Context, string, v1.Range, ...v1.Option) error); ok { + r2 = returnFunc(ctx, query, r, opts...) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// PrometheusAPIClient_QueryRange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryRange' +type PrometheusAPIClient_QueryRange_Call struct { + *mock.Call +} + +// QueryRange is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - r v1.Range +// - opts ...v1.Option +func (_e *PrometheusAPIClient_Expecter) QueryRange(ctx interface{}, query interface{}, r interface{}, opts ...interface{}) *PrometheusAPIClient_QueryRange_Call { + return &PrometheusAPIClient_QueryRange_Call{Call: _e.mock.On("QueryRange", + append([]interface{}{ctx, query, r}, opts...)...)} +} + +func (_c *PrometheusAPIClient_QueryRange_Call) Run(run func(ctx context.Context, query string, r v1.Range, opts ...v1.Option)) *PrometheusAPIClient_QueryRange_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 v1.Range + if args[2] != nil { + arg2 = args[2].(v1.Range) + } + var arg3 []v1.Option + var variadicArgs []v1.Option + if len(args) > 3 { + variadicArgs = args[3].([]v1.Option) + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_QueryRange_Call) Return(value model.Value, warnings v1.Warnings, err error) *PrometheusAPIClient_QueryRange_Call { + _c.Call.Return(value, warnings, err) + return _c +} + +func (_c *PrometheusAPIClient_QueryRange_Call) RunAndReturn(run func(ctx context.Context, query string, r v1.Range, opts ...v1.Option) (model.Value, v1.Warnings, error)) *PrometheusAPIClient_QueryRange_Call { + _c.Call.Return(run) + return _c +} + +// Rules provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) Rules(ctx context.Context) (v1.RulesResult, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Rules") + } + + var r0 v1.RulesResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) (v1.RulesResult, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) v1.RulesResult); ok { + r0 = returnFunc(ctx) + } else { + r0 = ret.Get(0).(v1.RulesResult) + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_Rules_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Rules' +type PrometheusAPIClient_Rules_Call struct { + *mock.Call +} + +// Rules is a helper method to define mock.On call +// - ctx context.Context +func (_e *PrometheusAPIClient_Expecter) Rules(ctx interface{}) *PrometheusAPIClient_Rules_Call { + return &PrometheusAPIClient_Rules_Call{Call: _e.mock.On("Rules", ctx)} +} + +func (_c *PrometheusAPIClient_Rules_Call) Run(run func(ctx context.Context)) *PrometheusAPIClient_Rules_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_Rules_Call) Return(rulesResult v1.RulesResult, err error) *PrometheusAPIClient_Rules_Call { + _c.Call.Return(rulesResult, err) + return _c +} + +func (_c *PrometheusAPIClient_Rules_Call) RunAndReturn(run func(ctx context.Context) (v1.RulesResult, error)) *PrometheusAPIClient_Rules_Call { + _c.Call.Return(run) + return _c +} + +// Runtimeinfo provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) Runtimeinfo(ctx context.Context) (v1.RuntimeinfoResult, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Runtimeinfo") + } + + var r0 v1.RuntimeinfoResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) (v1.RuntimeinfoResult, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) v1.RuntimeinfoResult); ok { + r0 = returnFunc(ctx) + } else { + r0 = ret.Get(0).(v1.RuntimeinfoResult) + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_Runtimeinfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Runtimeinfo' +type PrometheusAPIClient_Runtimeinfo_Call struct { + *mock.Call +} + +// Runtimeinfo is a helper method to define mock.On call +// - ctx context.Context +func (_e *PrometheusAPIClient_Expecter) Runtimeinfo(ctx interface{}) *PrometheusAPIClient_Runtimeinfo_Call { + return &PrometheusAPIClient_Runtimeinfo_Call{Call: _e.mock.On("Runtimeinfo", ctx)} +} + +func (_c *PrometheusAPIClient_Runtimeinfo_Call) Run(run func(ctx context.Context)) *PrometheusAPIClient_Runtimeinfo_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_Runtimeinfo_Call) Return(runtimeinfoResult v1.RuntimeinfoResult, err error) *PrometheusAPIClient_Runtimeinfo_Call { + _c.Call.Return(runtimeinfoResult, err) + return _c +} + +func (_c *PrometheusAPIClient_Runtimeinfo_Call) RunAndReturn(run func(ctx context.Context) (v1.RuntimeinfoResult, error)) *PrometheusAPIClient_Runtimeinfo_Call { + _c.Call.Return(run) + return _c +} + +// Series provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time, opts ...v1.Option) ([]model.LabelSet, v1.Warnings, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, matches, startTime, endTime, opts) + } else { + tmpRet = _mock.Called(ctx, matches, startTime, endTime) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for Series") + } + + var r0 []model.LabelSet + var r1 v1.Warnings + var r2 error + if returnFunc, ok := ret.Get(0).(func(context.Context, []string, time.Time, time.Time, ...v1.Option) ([]model.LabelSet, v1.Warnings, error)); ok { + return returnFunc(ctx, matches, startTime, endTime, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, []string, time.Time, time.Time, ...v1.Option) []model.LabelSet); ok { + r0 = returnFunc(ctx, matches, startTime, endTime, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]model.LabelSet) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, []string, time.Time, time.Time, ...v1.Option) v1.Warnings); ok { + r1 = returnFunc(ctx, matches, startTime, endTime, opts...) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(v1.Warnings) + } + } + if returnFunc, ok := ret.Get(2).(func(context.Context, []string, time.Time, time.Time, ...v1.Option) error); ok { + r2 = returnFunc(ctx, matches, startTime, endTime, opts...) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +// PrometheusAPIClient_Series_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Series' +type PrometheusAPIClient_Series_Call struct { + *mock.Call +} + +// Series is a helper method to define mock.On call +// - ctx context.Context +// - matches []string +// - startTime time.Time +// - endTime time.Time +// - opts ...v1.Option +func (_e *PrometheusAPIClient_Expecter) Series(ctx interface{}, matches interface{}, startTime interface{}, endTime interface{}, opts ...interface{}) *PrometheusAPIClient_Series_Call { + return &PrometheusAPIClient_Series_Call{Call: _e.mock.On("Series", + append([]interface{}{ctx, matches, startTime, endTime}, opts...)...)} +} + +func (_c *PrometheusAPIClient_Series_Call) Run(run func(ctx context.Context, matches []string, startTime time.Time, endTime time.Time, opts ...v1.Option)) *PrometheusAPIClient_Series_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + if args[1] != nil { + arg1 = args[1].([]string) + } + var arg2 time.Time + if args[2] != nil { + arg2 = args[2].(time.Time) + } + var arg3 time.Time + if args[3] != nil { + arg3 = args[3].(time.Time) + } + var arg4 []v1.Option + var variadicArgs []v1.Option + if len(args) > 4 { + variadicArgs = args[4].([]v1.Option) + } + arg4 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3, + arg4..., + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_Series_Call) Return(labelSets []model.LabelSet, warnings v1.Warnings, err error) *PrometheusAPIClient_Series_Call { + _c.Call.Return(labelSets, warnings, err) + return _c +} + +func (_c *PrometheusAPIClient_Series_Call) RunAndReturn(run func(ctx context.Context, matches []string, startTime time.Time, endTime time.Time, opts ...v1.Option) ([]model.LabelSet, v1.Warnings, error)) *PrometheusAPIClient_Series_Call { + _c.Call.Return(run) + return _c +} + +// Snapshot provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) Snapshot(ctx context.Context, skipHead bool) (v1.SnapshotResult, error) { + ret := _mock.Called(ctx, skipHead) + + if len(ret) == 0 { + panic("no return value specified for Snapshot") + } + + var r0 v1.SnapshotResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, bool) (v1.SnapshotResult, error)); ok { + return returnFunc(ctx, skipHead) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, bool) v1.SnapshotResult); ok { + r0 = returnFunc(ctx, skipHead) + } else { + r0 = ret.Get(0).(v1.SnapshotResult) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, bool) error); ok { + r1 = returnFunc(ctx, skipHead) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_Snapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Snapshot' +type PrometheusAPIClient_Snapshot_Call struct { + *mock.Call +} + +// Snapshot is a helper method to define mock.On call +// - ctx context.Context +// - skipHead bool +func (_e *PrometheusAPIClient_Expecter) Snapshot(ctx interface{}, skipHead interface{}) *PrometheusAPIClient_Snapshot_Call { + return &PrometheusAPIClient_Snapshot_Call{Call: _e.mock.On("Snapshot", ctx, skipHead)} +} + +func (_c *PrometheusAPIClient_Snapshot_Call) Run(run func(ctx context.Context, skipHead bool)) *PrometheusAPIClient_Snapshot_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 bool + if args[1] != nil { + arg1 = args[1].(bool) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_Snapshot_Call) Return(snapshotResult v1.SnapshotResult, err error) *PrometheusAPIClient_Snapshot_Call { + _c.Call.Return(snapshotResult, err) + return _c +} + +func (_c *PrometheusAPIClient_Snapshot_Call) RunAndReturn(run func(ctx context.Context, skipHead bool) (v1.SnapshotResult, error)) *PrometheusAPIClient_Snapshot_Call { + _c.Call.Return(run) + return _c +} + +// TSDB provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) TSDB(ctx context.Context, opts ...v1.Option) (v1.TSDBResult, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, opts) + } else { + tmpRet = _mock.Called(ctx) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for TSDB") + } + + var r0 v1.TSDBResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, ...v1.Option) (v1.TSDBResult, error)); ok { + return returnFunc(ctx, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, ...v1.Option) v1.TSDBResult); ok { + r0 = returnFunc(ctx, opts...) + } else { + r0 = ret.Get(0).(v1.TSDBResult) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, ...v1.Option) error); ok { + r1 = returnFunc(ctx, opts...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_TSDB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TSDB' +type PrometheusAPIClient_TSDB_Call struct { + *mock.Call +} + +// TSDB is a helper method to define mock.On call +// - ctx context.Context +// - opts ...v1.Option +func (_e *PrometheusAPIClient_Expecter) TSDB(ctx interface{}, opts ...interface{}) *PrometheusAPIClient_TSDB_Call { + return &PrometheusAPIClient_TSDB_Call{Call: _e.mock.On("TSDB", + append([]interface{}{ctx}, opts...)...)} +} + +func (_c *PrometheusAPIClient_TSDB_Call) Run(run func(ctx context.Context, opts ...v1.Option)) *PrometheusAPIClient_TSDB_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []v1.Option + var variadicArgs []v1.Option + if len(args) > 1 { + variadicArgs = args[1].([]v1.Option) + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_TSDB_Call) Return(tSDBResult v1.TSDBResult, err error) *PrometheusAPIClient_TSDB_Call { + _c.Call.Return(tSDBResult, err) + return _c +} + +func (_c *PrometheusAPIClient_TSDB_Call) RunAndReturn(run func(ctx context.Context, opts ...v1.Option) (v1.TSDBResult, error)) *PrometheusAPIClient_TSDB_Call { + _c.Call.Return(run) + return _c +} + +// Targets provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) Targets(ctx context.Context) (v1.TargetsResult, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Targets") + } + + var r0 v1.TargetsResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) (v1.TargetsResult, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) v1.TargetsResult); ok { + r0 = returnFunc(ctx) + } else { + r0 = ret.Get(0).(v1.TargetsResult) + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_Targets_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Targets' +type PrometheusAPIClient_Targets_Call struct { + *mock.Call +} + +// Targets is a helper method to define mock.On call +// - ctx context.Context +func (_e *PrometheusAPIClient_Expecter) Targets(ctx interface{}) *PrometheusAPIClient_Targets_Call { + return &PrometheusAPIClient_Targets_Call{Call: _e.mock.On("Targets", ctx)} +} + +func (_c *PrometheusAPIClient_Targets_Call) Run(run func(ctx context.Context)) *PrometheusAPIClient_Targets_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_Targets_Call) Return(targetsResult v1.TargetsResult, err error) *PrometheusAPIClient_Targets_Call { + _c.Call.Return(targetsResult, err) + return _c +} + +func (_c *PrometheusAPIClient_Targets_Call) RunAndReturn(run func(ctx context.Context) (v1.TargetsResult, error)) *PrometheusAPIClient_Targets_Call { + _c.Call.Return(run) + return _c +} + +// TargetsMetadata provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) TargetsMetadata(ctx context.Context, matchTarget string, metric string, limit string) ([]v1.MetricMetadata, error) { + ret := _mock.Called(ctx, matchTarget, metric, limit) + + if len(ret) == 0 { + panic("no return value specified for TargetsMetadata") + } + + var r0 []v1.MetricMetadata + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) ([]v1.MetricMetadata, error)); ok { + return returnFunc(ctx, matchTarget, metric, limit) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) []v1.MetricMetadata); ok { + r0 = returnFunc(ctx, matchTarget, metric, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]v1.MetricMetadata) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { + r1 = returnFunc(ctx, matchTarget, metric, limit) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_TargetsMetadata_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TargetsMetadata' +type PrometheusAPIClient_TargetsMetadata_Call struct { + *mock.Call +} + +// TargetsMetadata is a helper method to define mock.On call +// - ctx context.Context +// - matchTarget string +// - metric string +// - limit string +func (_e *PrometheusAPIClient_Expecter) TargetsMetadata(ctx interface{}, matchTarget interface{}, metric interface{}, limit interface{}) *PrometheusAPIClient_TargetsMetadata_Call { + return &PrometheusAPIClient_TargetsMetadata_Call{Call: _e.mock.On("TargetsMetadata", ctx, matchTarget, metric, limit)} +} + +func (_c *PrometheusAPIClient_TargetsMetadata_Call) Run(run func(ctx context.Context, matchTarget string, metric string, limit string)) *PrometheusAPIClient_TargetsMetadata_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_TargetsMetadata_Call) Return(metricMetadatas []v1.MetricMetadata, err error) *PrometheusAPIClient_TargetsMetadata_Call { + _c.Call.Return(metricMetadatas, err) + return _c +} + +func (_c *PrometheusAPIClient_TargetsMetadata_Call) RunAndReturn(run func(ctx context.Context, matchTarget string, metric string, limit string) ([]v1.MetricMetadata, error)) *PrometheusAPIClient_TargetsMetadata_Call { + _c.Call.Return(run) + return _c +} + +// WalReplay provides a mock function for the type PrometheusAPIClient +func (_mock *PrometheusAPIClient) WalReplay(ctx context.Context) (v1.WalReplayStatus, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for WalReplay") + } + + var r0 v1.WalReplayStatus + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) (v1.WalReplayStatus, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) v1.WalReplayStatus); ok { + r0 = returnFunc(ctx) + } else { + r0 = ret.Get(0).(v1.WalReplayStatus) + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// PrometheusAPIClient_WalReplay_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WalReplay' +type PrometheusAPIClient_WalReplay_Call struct { + *mock.Call +} + +// WalReplay is a helper method to define mock.On call +// - ctx context.Context +func (_e *PrometheusAPIClient_Expecter) WalReplay(ctx interface{}) *PrometheusAPIClient_WalReplay_Call { + return &PrometheusAPIClient_WalReplay_Call{Call: _e.mock.On("WalReplay", ctx)} +} + +func (_c *PrometheusAPIClient_WalReplay_Call) Run(run func(ctx context.Context)) *PrometheusAPIClient_WalReplay_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *PrometheusAPIClient_WalReplay_Call) Return(walReplayStatus v1.WalReplayStatus, err error) *PrometheusAPIClient_WalReplay_Call { + _c.Call.Return(walReplayStatus, err) + return _c +} + +func (_c *PrometheusAPIClient_WalReplay_Call) RunAndReturn(run func(ctx context.Context) (v1.WalReplayStatus, error)) *PrometheusAPIClient_WalReplay_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/http/backend/storage/search/search.go b/internal/http/backend/storage/search/search.go new file mode 100644 index 00000000..91381f94 --- /dev/null +++ b/internal/http/backend/storage/search/search.go @@ -0,0 +1,139 @@ +package search + +import ( + "context" + "strings" + "time" + + "github.com/lithammer/fuzzysearch/fuzzy" + + "github.com/slok/sloth/internal/http/backend/model" + "github.com/slok/sloth/internal/http/backend/storage" +) + +func NewSearchRepositoryWrapper(svcGetter storage.ServiceGetter, sloGetter storage.SLOGetter) (SearchRepositoryWrapper, error) { + return SearchRepositoryWrapper{ + svcGetter: svcGetter, + sloGetter: sloGetter, + }, nil +} + +// SearchRepositoryWrapper is a wrapper for repositories that implement search methods. +// This repository wrapper only acts on the search methods, the others will proxy to the +// actual implementation. +// +// The search method is done by fetching all data and filtering in memory using fuzzy search, so +// its not efficient but allows to have a common interface for search across different +// storage backends as a first iteration, we are already returning full lists from storage +// so filtering in memory is not a big deal yet. +type SearchRepositoryWrapper struct { + svcGetter storage.ServiceGetter + sloGetter storage.SLOGetter +} + +func (s SearchRepositoryWrapper) ListAllServiceAndAlerts(ctx context.Context) ([]storage.ServiceAndAlerts, error) { + return s.svcGetter.ListAllServiceAndAlerts(ctx) +} +func (s SearchRepositoryWrapper) ListServiceAndAlertsByServiceSearch(ctx context.Context, serviceSearchInput string) ([]storage.ServiceAndAlerts, error) { + services, err := s.ListAllServiceAndAlerts(ctx) + if err != nil { + return nil, err + } + + indexed := map[string]*storage.ServiceAndAlerts{} + servicesIDs := []string{} + for _, svc := range services { + servicesIDs = append(servicesIDs, svc.Service.ID) + indexed[svc.Service.ID] = &svc + } + + matches := find(serviceSearchInput, servicesIDs) + if len(matches) == 0 { + return nil, nil + } + + data := make([]storage.ServiceAndAlerts, 0, len(matches)) + for _, match := range matches { + data = append(data, *indexed[match]) + } + + return data, nil +} +func (s SearchRepositoryWrapper) ListSLOInstantDetailsService(ctx context.Context, serviceID string) ([]storage.SLOInstantDetails, error) { + return s.sloGetter.ListSLOInstantDetailsService(ctx, serviceID) +} +func (s SearchRepositoryWrapper) ListSLOInstantDetailsServiceBySLOSearch(ctx context.Context, serviceID, sloSearchInput string) ([]storage.SLOInstantDetails, error) { + slos, err := s.ListSLOInstantDetailsService(ctx, serviceID) + if err != nil { + return nil, err + } + + indexed := map[string]*storage.SLOInstantDetails{} + sloIDs := []string{} + for _, slo := range slos { + indexed[slo.SLO.ID] = &slo + sloIDs = append(sloIDs, slo.SLO.ID) + } + + matches := find(sloSearchInput, sloIDs) + if len(matches) == 0 { + return nil, nil + } + + data := make([]storage.SLOInstantDetails, 0, len(matches)) + for _, match := range matches { + data = append(data, *indexed[match]) + } + + return data, nil +} + +func (s SearchRepositoryWrapper) ListSLOInstantDetails(ctx context.Context) ([]storage.SLOInstantDetails, error) { + return s.sloGetter.ListSLOInstantDetails(ctx) +} + +func (s SearchRepositoryWrapper) ListSLOInstantDetailsBySLOSearch(ctx context.Context, sloSearchInput string) ([]storage.SLOInstantDetails, error) { + slos, err := s.ListSLOInstantDetails(ctx) + if err != nil { + return nil, err + } + + indexed := map[string]*storage.SLOInstantDetails{} + sloIDs := []string{} + for _, slo := range slos { + indexed[slo.SLO.ID] = &slo + sloIDs = append(sloIDs, slo.SLO.ID) + } + + matches := find(sloSearchInput, sloIDs) + if len(matches) == 0 { + return nil, nil + } + + data := make([]storage.SLOInstantDetails, 0, len(matches)) + for _, match := range matches { + data = append(data, *indexed[match]) + } + + return data, nil +} + +func (s SearchRepositoryWrapper) GetSLOInstantDetails(ctx context.Context, sloID string) (*storage.SLOInstantDetails, error) { + return s.sloGetter.GetSLOInstantDetails(ctx, sloID) +} + +func (s SearchRepositoryWrapper) GetSLIAvailabilityInRange(ctx context.Context, sloID string, from, to time.Time, step time.Duration) ([]model.DataPoint, error) { + return s.sloGetter.GetSLIAvailabilityInRange(ctx, sloID, from, to, step) + +} + +func (s SearchRepositoryWrapper) GetSLIAvailabilityInRangeAutoStep(ctx context.Context, sloID string, from, to time.Time) ([]model.DataPoint, error) { + return s.sloGetter.GetSLIAvailabilityInRangeAutoStep(ctx, sloID, from, to) +} + +func find(s string, ss []string) []string { + // Remove spaces for better matching. + s = strings.TrimSpace(s) + s = strings.ReplaceAll(s, " ", "") + return fuzzy.FindNormalizedFold(s, ss) +} diff --git a/internal/http/backend/storage/search/search_test.go b/internal/http/backend/storage/search/search_test.go new file mode 100644 index 00000000..cd3634e1 --- /dev/null +++ b/internal/http/backend/storage/search/search_test.go @@ -0,0 +1,207 @@ +package search_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/slok/sloth/internal/http/backend/model" + "github.com/slok/sloth/internal/http/backend/storage" + "github.com/slok/sloth/internal/http/backend/storage/search" + "github.com/slok/sloth/internal/http/backend/storage/storagemock" +) + +func TestSearchRepositoryWrapperListServiceAndAlertsByServiceSearch(t *testing.T) { + tests := map[string]struct { + searchInput string + mocks func(*storagemock.ServiceGetter, *storagemock.SLOGetter) + expRes []storage.ServiceAndAlerts + expErr bool + }{ + "Should return matching services when search input matches service IDs.": { + searchInput: "mt", + mocks: func(msg *storagemock.ServiceGetter, slg *storagemock.SLOGetter) { + msg.On("ListAllServiceAndAlerts", mock.Anything).Once().Return([]storage.ServiceAndAlerts{ + {Service: model.Service{ID: "service-mt"}}, + {Service: model.Service{ID: "service-xy"}}, + {Service: model.Service{ID: "another-service-mt"}}, + {Service: model.Service{ID: "unrelated"}}, + {Service: model.Service{ID: "service-mt-2"}}, + {Service: model.Service{ID: "service-z"}}, + }, nil) + }, + expRes: []storage.ServiceAndAlerts{ + {Service: model.Service{ID: "service-mt"}}, + {Service: model.Service{ID: "another-service-mt"}}, + {Service: model.Service{ID: "service-mt-2"}}, + }, + }, + + "Shouldn't return services when search input doesn't matches service IDs.": { + searchInput: "zzzzz", + mocks: func(msg *storagemock.ServiceGetter, slg *storagemock.SLOGetter) { + msg.On("ListAllServiceAndAlerts", mock.Anything).Once().Return([]storage.ServiceAndAlerts{ + {Service: model.Service{ID: "service-mt"}}, + {Service: model.Service{ID: "service-xy"}}, + {Service: model.Service{ID: "another-service-mt"}}, + {Service: model.Service{ID: "unrelated"}}, + {Service: model.Service{ID: "service-mt-2"}}, + {Service: model.Service{ID: "service-z"}}, + }, nil) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + msg := storagemock.NewServiceGetter(t) + slg := storagemock.NewSLOGetter(t) + test.mocks(msg, slg) + + repo, err := search.NewSearchRepositoryWrapper(msg, slg) + require.NoError(err) + + res, err := repo.ListServiceAndAlertsByServiceSearch(t.Context(), test.searchInput) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, res) + } + }) + } +} + +func TestSearchRepositoryWrapperListSLOInstantDetailsServiceBySLOSearch(t *testing.T) { + tests := map[string]struct { + service string + searchInput string + mocks func(*storagemock.ServiceGetter, *storagemock.SLOGetter) + expRes []storage.SLOInstantDetails + expErr bool + }{ + "Should return matching SLOs when search input matches SLO IDs.": { + service: "my-service", + searchInput: "mt", + mocks: func(msg *storagemock.ServiceGetter, slg *storagemock.SLOGetter) { + slg.On("ListSLOInstantDetailsService", mock.Anything, "my-service").Once().Return([]storage.SLOInstantDetails{ + {SLO: model.SLO{ID: "service-mt"}}, + {SLO: model.SLO{ID: "service-xy"}}, + {SLO: model.SLO{ID: "another-service-mt"}}, + {SLO: model.SLO{ID: "unrelated"}}, + {SLO: model.SLO{ID: "service-mt-2"}}, + {SLO: model.SLO{ID: "service-z"}}, + }, nil) + }, + expRes: []storage.SLOInstantDetails{ + {SLO: model.SLO{ID: "service-mt"}}, + {SLO: model.SLO{ID: "another-service-mt"}}, + {SLO: model.SLO{ID: "service-mt-2"}}, + }, + }, + + "Shouldn't return SLOs when search input doesn't matches SLO IDs.": { + service: "my-service", + searchInput: "zzzzz", + mocks: func(msg *storagemock.ServiceGetter, slg *storagemock.SLOGetter) { + slg.On("ListSLOInstantDetailsService", mock.Anything, "my-service").Once().Return([]storage.SLOInstantDetails{ + {SLO: model.SLO{ID: "service-mt"}}, + {SLO: model.SLO{ID: "service-xy"}}, + {SLO: model.SLO{ID: "another-service-mt"}}, + {SLO: model.SLO{ID: "unrelated"}}, + {SLO: model.SLO{ID: "service-mt-2"}}, + {SLO: model.SLO{ID: "service-z"}}, + }, nil) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + msg := storagemock.NewServiceGetter(t) + slg := storagemock.NewSLOGetter(t) + test.mocks(msg, slg) + + repo, err := search.NewSearchRepositoryWrapper(msg, slg) + require.NoError(err) + + res, err := repo.ListSLOInstantDetailsServiceBySLOSearch(t.Context(), test.service, test.searchInput) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, res) + } + }) + } + +} + +func TestSearchRepositoryWrapperListSLOInstantDetailsBySLOSearch(t *testing.T) { + tests := map[string]struct { + searchInput string + mocks func(*storagemock.ServiceGetter, *storagemock.SLOGetter) + expRes []storage.SLOInstantDetails + expErr bool + }{ + "Should return matching SLOs when search input matches SLO IDs.": { + searchInput: "mt", + mocks: func(msg *storagemock.ServiceGetter, slg *storagemock.SLOGetter) { + slg.On("ListSLOInstantDetails", mock.Anything).Once().Return([]storage.SLOInstantDetails{ + {SLO: model.SLO{ID: "service-mt"}}, + {SLO: model.SLO{ID: "service-xy"}}, + {SLO: model.SLO{ID: "another-service-mt"}}, + {SLO: model.SLO{ID: "unrelated"}}, + {SLO: model.SLO{ID: "service-mt-2"}}, + {SLO: model.SLO{ID: "service-z"}}, + }, nil) + }, + expRes: []storage.SLOInstantDetails{ + {SLO: model.SLO{ID: "service-mt"}}, + {SLO: model.SLO{ID: "another-service-mt"}}, + {SLO: model.SLO{ID: "service-mt-2"}}, + }, + }, + + "Shouldn't return SLOs when search input doesn't matches SLO IDs.": { + searchInput: "zzzzz", + mocks: func(msg *storagemock.ServiceGetter, slg *storagemock.SLOGetter) { + slg.On("ListSLOInstantDetails", mock.Anything).Once().Return([]storage.SLOInstantDetails{ + {SLO: model.SLO{ID: "service-mt"}}, + {SLO: model.SLO{ID: "service-xy"}}, + {SLO: model.SLO{ID: "another-service-mt"}}, + {SLO: model.SLO{ID: "unrelated"}}, + {SLO: model.SLO{ID: "service-mt-2"}}, + {SLO: model.SLO{ID: "service-z"}}, + }, nil) + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) + assert := assert.New(t) + + msg := storagemock.NewServiceGetter(t) + slg := storagemock.NewSLOGetter(t) + test.mocks(msg, slg) + + repo, err := search.NewSearchRepositoryWrapper(msg, slg) + require.NoError(err) + + res, err := repo.ListSLOInstantDetailsBySLOSearch(t.Context(), test.searchInput) + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expRes, res) + } + }) + } +} diff --git a/internal/http/backend/storage/storage.go b/internal/http/backend/storage/storage.go new file mode 100644 index 00000000..dc1a6b19 --- /dev/null +++ b/internal/http/backend/storage/storage.go @@ -0,0 +1,35 @@ +package storage + +import ( + "context" + "time" + + "github.com/slok/sloth/internal/http/backend/model" +) + +// ServiceAndAlerts groups a service with its SLO alerts. +type ServiceAndAlerts struct { + Service model.Service + Alerts []model.SLOAlerts +} + +type ServiceGetter interface { + ListAllServiceAndAlerts(ctx context.Context) ([]ServiceAndAlerts, error) + ListServiceAndAlertsByServiceSearch(ctx context.Context, serviceSearchInput string) ([]ServiceAndAlerts, error) +} + +type SLOInstantDetails struct { + SLO model.SLO + BudgetDetails model.SLOBudgetDetails + Alerts model.SLOAlerts +} + +type SLOGetter interface { + ListSLOInstantDetailsService(ctx context.Context, serviceID string) ([]SLOInstantDetails, error) + ListSLOInstantDetailsServiceBySLOSearch(ctx context.Context, serviceID, sloSearchInput string) ([]SLOInstantDetails, error) + ListSLOInstantDetails(ctx context.Context) ([]SLOInstantDetails, error) + ListSLOInstantDetailsBySLOSearch(ctx context.Context, sloSearchInput string) ([]SLOInstantDetails, error) + GetSLOInstantDetails(ctx context.Context, sloID string) (*SLOInstantDetails, error) + GetSLIAvailabilityInRange(ctx context.Context, sloID string, from, to time.Time, step time.Duration) ([]model.DataPoint, error) + GetSLIAvailabilityInRangeAutoStep(ctx context.Context, sloID string, from, to time.Time) ([]model.DataPoint, error) +} diff --git a/internal/http/backend/storage/storagemock/mocks.go b/internal/http/backend/storage/storagemock/mocks.go new file mode 100644 index 00000000..985a06e9 --- /dev/null +++ b/internal/http/backend/storage/storagemock/mocks.go @@ -0,0 +1,704 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package storagemock + +import ( + "context" + "time" + + "github.com/slok/sloth/internal/http/backend/model" + "github.com/slok/sloth/internal/http/backend/storage" + mock "github.com/stretchr/testify/mock" +) + +// NewServiceGetter creates a new instance of ServiceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewServiceGetter(t interface { + mock.TestingT + Cleanup(func()) +}) *ServiceGetter { + mock := &ServiceGetter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// ServiceGetter is an autogenerated mock type for the ServiceGetter type +type ServiceGetter struct { + mock.Mock +} + +type ServiceGetter_Expecter struct { + mock *mock.Mock +} + +func (_m *ServiceGetter) EXPECT() *ServiceGetter_Expecter { + return &ServiceGetter_Expecter{mock: &_m.Mock} +} + +// ListAllServiceAndAlerts provides a mock function for the type ServiceGetter +func (_mock *ServiceGetter) ListAllServiceAndAlerts(ctx context.Context) ([]storage.ServiceAndAlerts, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListAllServiceAndAlerts") + } + + var r0 []storage.ServiceAndAlerts + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) ([]storage.ServiceAndAlerts, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) []storage.ServiceAndAlerts); ok { + r0 = returnFunc(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]storage.ServiceAndAlerts) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// ServiceGetter_ListAllServiceAndAlerts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAllServiceAndAlerts' +type ServiceGetter_ListAllServiceAndAlerts_Call struct { + *mock.Call +} + +// ListAllServiceAndAlerts is a helper method to define mock.On call +// - ctx context.Context +func (_e *ServiceGetter_Expecter) ListAllServiceAndAlerts(ctx interface{}) *ServiceGetter_ListAllServiceAndAlerts_Call { + return &ServiceGetter_ListAllServiceAndAlerts_Call{Call: _e.mock.On("ListAllServiceAndAlerts", ctx)} +} + +func (_c *ServiceGetter_ListAllServiceAndAlerts_Call) Run(run func(ctx context.Context)) *ServiceGetter_ListAllServiceAndAlerts_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *ServiceGetter_ListAllServiceAndAlerts_Call) Return(serviceAndAlertss []storage.ServiceAndAlerts, err error) *ServiceGetter_ListAllServiceAndAlerts_Call { + _c.Call.Return(serviceAndAlertss, err) + return _c +} + +func (_c *ServiceGetter_ListAllServiceAndAlerts_Call) RunAndReturn(run func(ctx context.Context) ([]storage.ServiceAndAlerts, error)) *ServiceGetter_ListAllServiceAndAlerts_Call { + _c.Call.Return(run) + return _c +} + +// ListServiceAndAlertsByServiceSearch provides a mock function for the type ServiceGetter +func (_mock *ServiceGetter) ListServiceAndAlertsByServiceSearch(ctx context.Context, serviceSearchInput string) ([]storage.ServiceAndAlerts, error) { + ret := _mock.Called(ctx, serviceSearchInput) + + if len(ret) == 0 { + panic("no return value specified for ListServiceAndAlertsByServiceSearch") + } + + var r0 []storage.ServiceAndAlerts + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]storage.ServiceAndAlerts, error)); ok { + return returnFunc(ctx, serviceSearchInput) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) []storage.ServiceAndAlerts); ok { + r0 = returnFunc(ctx, serviceSearchInput) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]storage.ServiceAndAlerts) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, serviceSearchInput) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// ServiceGetter_ListServiceAndAlertsByServiceSearch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListServiceAndAlertsByServiceSearch' +type ServiceGetter_ListServiceAndAlertsByServiceSearch_Call struct { + *mock.Call +} + +// ListServiceAndAlertsByServiceSearch is a helper method to define mock.On call +// - ctx context.Context +// - serviceSearchInput string +func (_e *ServiceGetter_Expecter) ListServiceAndAlertsByServiceSearch(ctx interface{}, serviceSearchInput interface{}) *ServiceGetter_ListServiceAndAlertsByServiceSearch_Call { + return &ServiceGetter_ListServiceAndAlertsByServiceSearch_Call{Call: _e.mock.On("ListServiceAndAlertsByServiceSearch", ctx, serviceSearchInput)} +} + +func (_c *ServiceGetter_ListServiceAndAlertsByServiceSearch_Call) Run(run func(ctx context.Context, serviceSearchInput string)) *ServiceGetter_ListServiceAndAlertsByServiceSearch_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *ServiceGetter_ListServiceAndAlertsByServiceSearch_Call) Return(serviceAndAlertss []storage.ServiceAndAlerts, err error) *ServiceGetter_ListServiceAndAlertsByServiceSearch_Call { + _c.Call.Return(serviceAndAlertss, err) + return _c +} + +func (_c *ServiceGetter_ListServiceAndAlertsByServiceSearch_Call) RunAndReturn(run func(ctx context.Context, serviceSearchInput string) ([]storage.ServiceAndAlerts, error)) *ServiceGetter_ListServiceAndAlertsByServiceSearch_Call { + _c.Call.Return(run) + return _c +} + +// NewSLOGetter creates a new instance of SLOGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSLOGetter(t interface { + mock.TestingT + Cleanup(func()) +}) *SLOGetter { + mock := &SLOGetter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// SLOGetter is an autogenerated mock type for the SLOGetter type +type SLOGetter struct { + mock.Mock +} + +type SLOGetter_Expecter struct { + mock *mock.Mock +} + +func (_m *SLOGetter) EXPECT() *SLOGetter_Expecter { + return &SLOGetter_Expecter{mock: &_m.Mock} +} + +// GetSLIAvailabilityInRange provides a mock function for the type SLOGetter +func (_mock *SLOGetter) GetSLIAvailabilityInRange(ctx context.Context, sloID string, from time.Time, to time.Time, step time.Duration) ([]model.DataPoint, error) { + ret := _mock.Called(ctx, sloID, from, to, step) + + if len(ret) == 0 { + panic("no return value specified for GetSLIAvailabilityInRange") + } + + var r0 []model.DataPoint + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time, time.Time, time.Duration) ([]model.DataPoint, error)); ok { + return returnFunc(ctx, sloID, from, to, step) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time, time.Time, time.Duration) []model.DataPoint); ok { + r0 = returnFunc(ctx, sloID, from, to, step) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]model.DataPoint) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, time.Time, time.Time, time.Duration) error); ok { + r1 = returnFunc(ctx, sloID, from, to, step) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// SLOGetter_GetSLIAvailabilityInRange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSLIAvailabilityInRange' +type SLOGetter_GetSLIAvailabilityInRange_Call struct { + *mock.Call +} + +// GetSLIAvailabilityInRange is a helper method to define mock.On call +// - ctx context.Context +// - sloID string +// - from time.Time +// - to time.Time +// - step time.Duration +func (_e *SLOGetter_Expecter) GetSLIAvailabilityInRange(ctx interface{}, sloID interface{}, from interface{}, to interface{}, step interface{}) *SLOGetter_GetSLIAvailabilityInRange_Call { + return &SLOGetter_GetSLIAvailabilityInRange_Call{Call: _e.mock.On("GetSLIAvailabilityInRange", ctx, sloID, from, to, step)} +} + +func (_c *SLOGetter_GetSLIAvailabilityInRange_Call) Run(run func(ctx context.Context, sloID string, from time.Time, to time.Time, step time.Duration)) *SLOGetter_GetSLIAvailabilityInRange_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 time.Time + if args[2] != nil { + arg2 = args[2].(time.Time) + } + var arg3 time.Time + if args[3] != nil { + arg3 = args[3].(time.Time) + } + var arg4 time.Duration + if args[4] != nil { + arg4 = args[4].(time.Duration) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + ) + }) + return _c +} + +func (_c *SLOGetter_GetSLIAvailabilityInRange_Call) Return(dataPoints []model.DataPoint, err error) *SLOGetter_GetSLIAvailabilityInRange_Call { + _c.Call.Return(dataPoints, err) + return _c +} + +func (_c *SLOGetter_GetSLIAvailabilityInRange_Call) RunAndReturn(run func(ctx context.Context, sloID string, from time.Time, to time.Time, step time.Duration) ([]model.DataPoint, error)) *SLOGetter_GetSLIAvailabilityInRange_Call { + _c.Call.Return(run) + return _c +} + +// GetSLIAvailabilityInRangeAutoStep provides a mock function for the type SLOGetter +func (_mock *SLOGetter) GetSLIAvailabilityInRangeAutoStep(ctx context.Context, sloID string, from time.Time, to time.Time) ([]model.DataPoint, error) { + ret := _mock.Called(ctx, sloID, from, to) + + if len(ret) == 0 { + panic("no return value specified for GetSLIAvailabilityInRangeAutoStep") + } + + var r0 []model.DataPoint + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time, time.Time) ([]model.DataPoint, error)); ok { + return returnFunc(ctx, sloID, from, to) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time, time.Time) []model.DataPoint); ok { + r0 = returnFunc(ctx, sloID, from, to) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]model.DataPoint) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, time.Time, time.Time) error); ok { + r1 = returnFunc(ctx, sloID, from, to) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// SLOGetter_GetSLIAvailabilityInRangeAutoStep_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSLIAvailabilityInRangeAutoStep' +type SLOGetter_GetSLIAvailabilityInRangeAutoStep_Call struct { + *mock.Call +} + +// GetSLIAvailabilityInRangeAutoStep is a helper method to define mock.On call +// - ctx context.Context +// - sloID string +// - from time.Time +// - to time.Time +func (_e *SLOGetter_Expecter) GetSLIAvailabilityInRangeAutoStep(ctx interface{}, sloID interface{}, from interface{}, to interface{}) *SLOGetter_GetSLIAvailabilityInRangeAutoStep_Call { + return &SLOGetter_GetSLIAvailabilityInRangeAutoStep_Call{Call: _e.mock.On("GetSLIAvailabilityInRangeAutoStep", ctx, sloID, from, to)} +} + +func (_c *SLOGetter_GetSLIAvailabilityInRangeAutoStep_Call) Run(run func(ctx context.Context, sloID string, from time.Time, to time.Time)) *SLOGetter_GetSLIAvailabilityInRangeAutoStep_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 time.Time + if args[2] != nil { + arg2 = args[2].(time.Time) + } + var arg3 time.Time + if args[3] != nil { + arg3 = args[3].(time.Time) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *SLOGetter_GetSLIAvailabilityInRangeAutoStep_Call) Return(dataPoints []model.DataPoint, err error) *SLOGetter_GetSLIAvailabilityInRangeAutoStep_Call { + _c.Call.Return(dataPoints, err) + return _c +} + +func (_c *SLOGetter_GetSLIAvailabilityInRangeAutoStep_Call) RunAndReturn(run func(ctx context.Context, sloID string, from time.Time, to time.Time) ([]model.DataPoint, error)) *SLOGetter_GetSLIAvailabilityInRangeAutoStep_Call { + _c.Call.Return(run) + return _c +} + +// GetSLOInstantDetails provides a mock function for the type SLOGetter +func (_mock *SLOGetter) GetSLOInstantDetails(ctx context.Context, sloID string) (*storage.SLOInstantDetails, error) { + ret := _mock.Called(ctx, sloID) + + if len(ret) == 0 { + panic("no return value specified for GetSLOInstantDetails") + } + + var r0 *storage.SLOInstantDetails + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*storage.SLOInstantDetails, error)); ok { + return returnFunc(ctx, sloID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *storage.SLOInstantDetails); ok { + r0 = returnFunc(ctx, sloID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*storage.SLOInstantDetails) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, sloID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// SLOGetter_GetSLOInstantDetails_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSLOInstantDetails' +type SLOGetter_GetSLOInstantDetails_Call struct { + *mock.Call +} + +// GetSLOInstantDetails is a helper method to define mock.On call +// - ctx context.Context +// - sloID string +func (_e *SLOGetter_Expecter) GetSLOInstantDetails(ctx interface{}, sloID interface{}) *SLOGetter_GetSLOInstantDetails_Call { + return &SLOGetter_GetSLOInstantDetails_Call{Call: _e.mock.On("GetSLOInstantDetails", ctx, sloID)} +} + +func (_c *SLOGetter_GetSLOInstantDetails_Call) Run(run func(ctx context.Context, sloID string)) *SLOGetter_GetSLOInstantDetails_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *SLOGetter_GetSLOInstantDetails_Call) Return(sLOInstantDetails *storage.SLOInstantDetails, err error) *SLOGetter_GetSLOInstantDetails_Call { + _c.Call.Return(sLOInstantDetails, err) + return _c +} + +func (_c *SLOGetter_GetSLOInstantDetails_Call) RunAndReturn(run func(ctx context.Context, sloID string) (*storage.SLOInstantDetails, error)) *SLOGetter_GetSLOInstantDetails_Call { + _c.Call.Return(run) + return _c +} + +// ListSLOInstantDetails provides a mock function for the type SLOGetter +func (_mock *SLOGetter) ListSLOInstantDetails(ctx context.Context) ([]storage.SLOInstantDetails, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListSLOInstantDetails") + } + + var r0 []storage.SLOInstantDetails + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) ([]storage.SLOInstantDetails, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) []storage.SLOInstantDetails); ok { + r0 = returnFunc(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]storage.SLOInstantDetails) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// SLOGetter_ListSLOInstantDetails_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListSLOInstantDetails' +type SLOGetter_ListSLOInstantDetails_Call struct { + *mock.Call +} + +// ListSLOInstantDetails is a helper method to define mock.On call +// - ctx context.Context +func (_e *SLOGetter_Expecter) ListSLOInstantDetails(ctx interface{}) *SLOGetter_ListSLOInstantDetails_Call { + return &SLOGetter_ListSLOInstantDetails_Call{Call: _e.mock.On("ListSLOInstantDetails", ctx)} +} + +func (_c *SLOGetter_ListSLOInstantDetails_Call) Run(run func(ctx context.Context)) *SLOGetter_ListSLOInstantDetails_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *SLOGetter_ListSLOInstantDetails_Call) Return(sLOInstantDetailss []storage.SLOInstantDetails, err error) *SLOGetter_ListSLOInstantDetails_Call { + _c.Call.Return(sLOInstantDetailss, err) + return _c +} + +func (_c *SLOGetter_ListSLOInstantDetails_Call) RunAndReturn(run func(ctx context.Context) ([]storage.SLOInstantDetails, error)) *SLOGetter_ListSLOInstantDetails_Call { + _c.Call.Return(run) + return _c +} + +// ListSLOInstantDetailsBySLOSearch provides a mock function for the type SLOGetter +func (_mock *SLOGetter) ListSLOInstantDetailsBySLOSearch(ctx context.Context, sloSearchInput string) ([]storage.SLOInstantDetails, error) { + ret := _mock.Called(ctx, sloSearchInput) + + if len(ret) == 0 { + panic("no return value specified for ListSLOInstantDetailsBySLOSearch") + } + + var r0 []storage.SLOInstantDetails + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]storage.SLOInstantDetails, error)); ok { + return returnFunc(ctx, sloSearchInput) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) []storage.SLOInstantDetails); ok { + r0 = returnFunc(ctx, sloSearchInput) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]storage.SLOInstantDetails) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, sloSearchInput) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// SLOGetter_ListSLOInstantDetailsBySLOSearch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListSLOInstantDetailsBySLOSearch' +type SLOGetter_ListSLOInstantDetailsBySLOSearch_Call struct { + *mock.Call +} + +// ListSLOInstantDetailsBySLOSearch is a helper method to define mock.On call +// - ctx context.Context +// - sloSearchInput string +func (_e *SLOGetter_Expecter) ListSLOInstantDetailsBySLOSearch(ctx interface{}, sloSearchInput interface{}) *SLOGetter_ListSLOInstantDetailsBySLOSearch_Call { + return &SLOGetter_ListSLOInstantDetailsBySLOSearch_Call{Call: _e.mock.On("ListSLOInstantDetailsBySLOSearch", ctx, sloSearchInput)} +} + +func (_c *SLOGetter_ListSLOInstantDetailsBySLOSearch_Call) Run(run func(ctx context.Context, sloSearchInput string)) *SLOGetter_ListSLOInstantDetailsBySLOSearch_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *SLOGetter_ListSLOInstantDetailsBySLOSearch_Call) Return(sLOInstantDetailss []storage.SLOInstantDetails, err error) *SLOGetter_ListSLOInstantDetailsBySLOSearch_Call { + _c.Call.Return(sLOInstantDetailss, err) + return _c +} + +func (_c *SLOGetter_ListSLOInstantDetailsBySLOSearch_Call) RunAndReturn(run func(ctx context.Context, sloSearchInput string) ([]storage.SLOInstantDetails, error)) *SLOGetter_ListSLOInstantDetailsBySLOSearch_Call { + _c.Call.Return(run) + return _c +} + +// ListSLOInstantDetailsService provides a mock function for the type SLOGetter +func (_mock *SLOGetter) ListSLOInstantDetailsService(ctx context.Context, serviceID string) ([]storage.SLOInstantDetails, error) { + ret := _mock.Called(ctx, serviceID) + + if len(ret) == 0 { + panic("no return value specified for ListSLOInstantDetailsService") + } + + var r0 []storage.SLOInstantDetails + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]storage.SLOInstantDetails, error)); ok { + return returnFunc(ctx, serviceID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) []storage.SLOInstantDetails); ok { + r0 = returnFunc(ctx, serviceID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]storage.SLOInstantDetails) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, serviceID) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// SLOGetter_ListSLOInstantDetailsService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListSLOInstantDetailsService' +type SLOGetter_ListSLOInstantDetailsService_Call struct { + *mock.Call +} + +// ListSLOInstantDetailsService is a helper method to define mock.On call +// - ctx context.Context +// - serviceID string +func (_e *SLOGetter_Expecter) ListSLOInstantDetailsService(ctx interface{}, serviceID interface{}) *SLOGetter_ListSLOInstantDetailsService_Call { + return &SLOGetter_ListSLOInstantDetailsService_Call{Call: _e.mock.On("ListSLOInstantDetailsService", ctx, serviceID)} +} + +func (_c *SLOGetter_ListSLOInstantDetailsService_Call) Run(run func(ctx context.Context, serviceID string)) *SLOGetter_ListSLOInstantDetailsService_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *SLOGetter_ListSLOInstantDetailsService_Call) Return(sLOInstantDetailss []storage.SLOInstantDetails, err error) *SLOGetter_ListSLOInstantDetailsService_Call { + _c.Call.Return(sLOInstantDetailss, err) + return _c +} + +func (_c *SLOGetter_ListSLOInstantDetailsService_Call) RunAndReturn(run func(ctx context.Context, serviceID string) ([]storage.SLOInstantDetails, error)) *SLOGetter_ListSLOInstantDetailsService_Call { + _c.Call.Return(run) + return _c +} + +// ListSLOInstantDetailsServiceBySLOSearch provides a mock function for the type SLOGetter +func (_mock *SLOGetter) ListSLOInstantDetailsServiceBySLOSearch(ctx context.Context, serviceID string, sloSearchInput string) ([]storage.SLOInstantDetails, error) { + ret := _mock.Called(ctx, serviceID, sloSearchInput) + + if len(ret) == 0 { + panic("no return value specified for ListSLOInstantDetailsServiceBySLOSearch") + } + + var r0 []storage.SLOInstantDetails + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) ([]storage.SLOInstantDetails, error)); ok { + return returnFunc(ctx, serviceID, sloSearchInput) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) []storage.SLOInstantDetails); ok { + r0 = returnFunc(ctx, serviceID, sloSearchInput) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]storage.SLOInstantDetails) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = returnFunc(ctx, serviceID, sloSearchInput) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// SLOGetter_ListSLOInstantDetailsServiceBySLOSearch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListSLOInstantDetailsServiceBySLOSearch' +type SLOGetter_ListSLOInstantDetailsServiceBySLOSearch_Call struct { + *mock.Call +} + +// ListSLOInstantDetailsServiceBySLOSearch is a helper method to define mock.On call +// - ctx context.Context +// - serviceID string +// - sloSearchInput string +func (_e *SLOGetter_Expecter) ListSLOInstantDetailsServiceBySLOSearch(ctx interface{}, serviceID interface{}, sloSearchInput interface{}) *SLOGetter_ListSLOInstantDetailsServiceBySLOSearch_Call { + return &SLOGetter_ListSLOInstantDetailsServiceBySLOSearch_Call{Call: _e.mock.On("ListSLOInstantDetailsServiceBySLOSearch", ctx, serviceID, sloSearchInput)} +} + +func (_c *SLOGetter_ListSLOInstantDetailsServiceBySLOSearch_Call) Run(run func(ctx context.Context, serviceID string, sloSearchInput string)) *SLOGetter_ListSLOInstantDetailsServiceBySLOSearch_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *SLOGetter_ListSLOInstantDetailsServiceBySLOSearch_Call) Return(sLOInstantDetailss []storage.SLOInstantDetails, err error) *SLOGetter_ListSLOInstantDetailsServiceBySLOSearch_Call { + _c.Call.Return(sLOInstantDetailss, err) + return _c +} + +func (_c *SLOGetter_ListSLOInstantDetailsServiceBySLOSearch_Call) RunAndReturn(run func(ctx context.Context, serviceID string, sloSearchInput string) ([]storage.SLOInstantDetails, error)) *SLOGetter_ListSLOInstantDetailsServiceBySLOSearch_Call { + _c.Call.Return(run) + return _c +} diff --git a/internal/http/ui/common_test.go b/internal/http/ui/common_test.go new file mode 100644 index 00000000..a7d88db2 --- /dev/null +++ b/internal/http/ui/common_test.go @@ -0,0 +1,53 @@ +package ui_test + +import ( + "net/http" + "net/http/httptest" + "regexp" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/slok/sloth/internal/http/ui" + "github.com/slok/sloth/internal/http/ui/uimock" +) + +var trimSpaceMultilineRegexp = regexp.MustCompile(`(?m)(^\s+|\s+$)`) + +func assertContainsHTTPResponseBody(t *testing.T, exp []string, resp *httptest.ResponseRecorder) { + // Sanitize got HTML so we make easier to check content. + got := resp.Body.String() + got = trimSpaceMultilineRegexp.ReplaceAllString(got, "") + got = strings.ReplaceAll(got, "\n", " ") + + // Check each expected snippet. + for _, e := range exp { + assert.Contains(t, got, e) + } +} + +type mocks struct { + ServiceApp *uimock.ServiceApp +} + +func newMocks(t *testing.T) mocks { + return mocks{ + ServiceApp: &uimock.ServiceApp{}, + } +} + +// Always now is an specific time for tests idempotency. +var testTimeNow, _ = time.Parse(time.RFC3339, "2025-11-15T01:02:03Z") + +func newTestUIHandler(t *testing.T, m mocks) http.Handler { + h, err := ui.NewUI(ui.UIConfig{ + ServiceApp: m.ServiceApp, + TimeNowFunc: func() time.Time { return testTimeNow }, + }) + require.NoError(t, err) + + return h +} diff --git a/internal/http/ui/common_tpl.go b/internal/http/ui/common_tpl.go new file mode 100644 index 00000000..3cc12a27 --- /dev/null +++ b/internal/http/ui/common_tpl.go @@ -0,0 +1,74 @@ +package ui + +import ( + "fmt" + "math" + "strconv" +) + +func prettyPercent(f float64) string { + return prettyPercentFixed(f, 2) +} + +func prettyPercentFixed(f float64, decimalNumber uint) string { + ff := roundFloat(f, decimalNumber) + return strconv.FormatFloat(ff, 'f', -1, 64) + "%" +} + +func roundFloat(val float64, precision uint) float64 { + ratio := math.Pow(10, float64(precision)) + return math.Round(val*ratio) / ratio +} + +func PercentUpColorCSSClass(f float64) string { + return PercentColorCSSClassCustom(f, 90.0, false) +} + +func PercentDownColorCSSClass(f float64) string { + return PercentColorCSSClassCustom(f, 10.0, true) +} + +// PercentColorCSSClassCustom returns a CSS class based on the given percent value (f). +// If inverse is true, lower values are considered worse. +// warningThreshold defines the threshold for warning state. +func PercentColorCSSClassCustom(f float64, warningThreshold float64, inverse bool) string { + const ( + cssClassOK = "is-ok" + cssClassWarning = "is-warning" + cssClassDanger = "is-critical" + ) + if inverse { + if f < 0 { + return cssClassDanger + } else if f < warningThreshold { + return cssClassWarning + } + + } else { + if f > 100 { + return cssClassDanger + } else if f >= warningThreshold { + return cssClassWarning + } + } + + return cssClassOK +} + +func SlothLogoSVG(color string, width int) string { + const svgFmt = ` + + + + +` + if color == "" { + color = "#EC604D" + } + + if width <= 0 { + width = 1525 + } + + return fmt.Sprintf(svgFmt, width, color) +} diff --git a/internal/http/ui/common_uplot.go b/internal/http/ui/common_uplot.go new file mode 100644 index 00000000..6888904a --- /dev/null +++ b/internal/http/ui/common_uplot.go @@ -0,0 +1,63 @@ +package ui + +// Check: https://github.com/leeoniya/uPlot/tree/master/docs. +type uPlotSLIChart struct { + Title string `json:"title"` + ColorSLI string `json:"color_sli"` + ColorObjective string `json:"color_objective"` + Width int `json:"width"` + Height int `json:"height"` + TSs []int `json:"timestamps"` + SLIs []*float64 `json:"sli_values"` + SLOObjective float64 `json:"slo_objective"` +} + +func (u *uPlotSLIChart) defaults() error { + if u.Title == "" { + u.Title = "SLI over time" + } + if u.ColorSLI == "" { + u.ColorSLI = "#017FC0" + } + if u.ColorObjective == "" { + u.ColorObjective = "#d63031" + } + + if u.Height == 0 { + u.Height = 400 + } + return nil +} + +// Check: https://github.com/leeoniya/uPlot/tree/master/docs. +type uPlotBudgetBurnChart struct { + Title string `json:"title"` + ColorReal string `json:"color_real"` + ColorPerfect string `json:"color_perfect"` + Width int `json:"width"` + Height int `json:"height"` + TSs []int `json:"timestamps"` + RealBurned []*float64 `json:"real_burned_values"` + PerfectBurned []*float64 `json:"perfect_burned_values"` +} + +func (u *uPlotBudgetBurnChart) defaults() error { + if u.Title == "" { + u.Title = "Budget Burn" + } + if u.ColorReal == "" { + u.ColorReal = "#017FC0" + } + if u.ColorPerfect == "" { + u.ColorPerfect = "#d63031" + } + + if u.Height == 0 { + u.Height = 400 + } + return nil +} + +func float64Ptr(f float64) *float64 { + return &f +} diff --git a/internal/http/ui/common_urls.go b/internal/http/ui/common_urls.go new file mode 100644 index 00000000..4e56a122 --- /dev/null +++ b/internal/http/ui/common_urls.go @@ -0,0 +1,89 @@ +package ui + +import ( + "fmt" + "net/http" + "strings" + + "github.com/slok/sloth/internal/http/ui/htmx" +) + +const ( + queryParamComponent = "component" + queryParamForwardCursor = "forward-cursor" + queryParamBackwardCursor = "backward-cursor" +) + +// urls is a common url manager with common utilities around URLs so they are handled in a single place. +var urls = urlManager{} + +type urlManager struct{} + +// ComponentFromRequest will return the component from a request (empty if doesn't have). +func (u urlManager) ComponentFromRequest(r *http.Request) string { + return r.URL.Query().Get(queryParamComponent) +} + +// URLWithComponent will return a URL that adds a component to the URL. +func (u urlManager) URLWithComponent(url string, component string) string { + return u.AddQueryParm(url, queryParamComponent, component) +} + +// ForwardCursorFromRequest will return the cursor from a request (empty if doesn't have). +func (u urlManager) ForwardCursorFromRequest(r *http.Request) string { + return r.URL.Query().Get(queryParamForwardCursor) +} + +// URLWithForwardCursor will return a URL that adds a cursor for the next elements. +func (u urlManager) URLWithForwardCursor(url string, cursor string) string { + return u.AddQueryParm(url, queryParamForwardCursor, cursor) +} + +// BackwardCursorFromRequest will return the cursor from a request (empty if doesn't have). +func (u urlManager) BackwardCursorFromRequest(r *http.Request) string { + return r.URL.Query().Get(queryParamBackwardCursor) +} + +// URLWithBackwardCursor will return a URL that adds a cursor for the previous elements. +func (u urlManager) URLWithBackwardCursor(url string, cursor string) string { + return u.AddQueryParm(url, queryParamBackwardCursor, cursor) +} + +// NonAppURL will return a URL that is not part of the app (before the user has been logged in). +func (u urlManager) NonAppURL(url string) string { + return ServePrefix + url +} + +// AppURL will return a URL that is part of the app (user logged) but is not part of an organization. +func (u urlManager) AppURL(url string) string { + return ServePrefix + URLPathAppPrefix + url +} + +// RedirectToIndex will redirect to the index. +func (u urlManager) RedirectToIndex(w http.ResponseWriter, r *http.Request) { + u.RedirectToURL(w, r, u.NonAppURL("")) +} + +// RedirectToApp will redirect to the app index. +func (u urlManager) RedirectToApp(w http.ResponseWriter, r *http.Request) { + u.RedirectToURL(w, r, u.AppURL("")) +} + +func (u urlManager) RedirectToURL(w http.ResponseWriter, r *http.Request, url string) { + // If HTMX request, redirect with HTMX, if not regular redirect. + if htmx.NewRequest(r.Header).IsHTMXRequest() { + htmx.NewResponse().WithRedirect(url).SetHeaders(w) + return + } + + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func (u urlManager) AddQueryParm(url, key, value string) string { + queryParamFmt := "?%s=%s" + if strings.Contains(url, "?") { + queryParamFmt = "&%s=%s" + } + + return url + fmt.Sprintf(queryParamFmt, key, value) +} diff --git a/internal/http/ui/handler_index.go b/internal/http/ui/handler_index.go new file mode 100644 index 00000000..6ebad216 --- /dev/null +++ b/internal/http/ui/handler_index.go @@ -0,0 +1,11 @@ +package ui + +import ( + "net/http" +) + +func (u ui) handlerIndex() http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + urls.RedirectToURL(w, r, urls.AppURL("/services")) + }) +} diff --git a/internal/http/ui/handler_index_test.go b/internal/http/ui/handler_index_test.go new file mode 100644 index 00000000..b10bbb1d --- /dev/null +++ b/internal/http/ui/handler_index_test.go @@ -0,0 +1,51 @@ +package ui_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHandlerIndex(t *testing.T) { + tests := map[string]struct { + request func() *http.Request + mock func(m mocks) + expBody []string + expHeaders http.Header + expCode int + }{ + "Entering the index should redirect to the services selection page.": { + request: func() *http.Request { + return httptest.NewRequest(http.MethodGet, "/u", nil) + }, + mock: func(m mocks) {}, + expHeaders: http.Header{ + "Content-Type": {"text/html; charset=utf-8"}, + "Location": {"/u/app/services"}, + }, + expCode: 307, + expBody: []string{}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + m := newMocks(t) + test.mock(m) + + h := newTestUIHandler(t, m) + + w := httptest.NewRecorder() + h.ServeHTTP(w, test.request()) + + assert.Equal(test.expCode, w.Code) + assert.Equal(test.expHeaders, w.Header()) + assertContainsHTTPResponseBody(t, test.expBody, w) + }) + + } +} diff --git a/internal/http/ui/handler_select_service.go b/internal/http/ui/handler_select_service.go new file mode 100644 index 00000000..747b2488 --- /dev/null +++ b/internal/http/ui/handler_select_service.go @@ -0,0 +1,162 @@ +package ui + +import ( + "net/http" + + "github.com/slok/sloth/internal/http/backend/app" + "github.com/slok/sloth/internal/http/ui/htmx" +) + +type tplPaginationData struct { + HasNext bool + HasPrevious bool + NextURL string + PrevURL string +} + +func mapPaginationToTPL(cursors app.PaginationCursors, baseURL string) tplPaginationData { + pagination := tplPaginationData{ + HasNext: cursors.HasNext, + HasPrevious: cursors.HasPrevious, + } + if cursors.HasNext { + pagination.NextURL = urls.URLWithForwardCursor(baseURL, cursors.NextCursor) + } + if cursors.HasPrevious { + pagination.PrevURL = urls.URLWithBackwardCursor(baseURL, cursors.PrevCursor) + } + return pagination +} + +func (u ui) handlerSelectService() http.HandlerFunc { + + const ( + componentServiceList = "service-list" + ) + + const ( + queryParamServiceSearch = "service-search" + ) + + type tplDataService struct { + Name string + HasWarning bool + HasCritical bool + DetailsURL string + } + type tplData struct { + Services []tplDataService + ServicePagination tplPaginationData + ServiceSearchURL string + ServiceSearchInput string + } + + mapServiceToTPL := func(s []app.ServiceAlerts) []tplDataService { + tplServices := make([]tplDataService, 0, len(s)) + for _, svc := range s { + hasCritical := false + hasWarning := false + for _, sloAlert := range svc.Alerts { + if sloAlert.FiringPage != nil { + hasCritical = true + } + if sloAlert.FiringWarning != nil { + hasWarning = true + } + if hasCritical && hasWarning { + break + } + } + tplServices = append(tplServices, tplDataService{ + Name: svc.Service.ID, + HasWarning: hasWarning, + HasCritical: hasCritical, + DetailsURL: urls.AppURL("/services/" + svc.Service.ID), + }) + } + return tplServices + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + isHTMXCall := htmx.NewRequest(r.Header).IsHTMXRequest() + component := urls.ComponentFromRequest(r) + + data := tplData{ + ServiceSearchURL: urls.URLWithComponent(urls.AppURL("/services"), componentServiceList), + } + + nextCursor := urls.ForwardCursorFromRequest(r) + prevCursor := urls.BackwardCursorFromRequest(r) + data.ServiceSearchInput = r.URL.Query().Get(queryParamServiceSearch) + currentURL := urls.AppURL("/services") + if data.ServiceSearchInput != "" { + currentURL = urls.AddQueryParm(currentURL, queryParamServiceSearch, data.ServiceSearchInput) + } + htmx.NewResponse().WithPushURL(currentURL).SetHeaders(w) // Always push URL with search or no search param. + + switch { + // Snippet service list next. + case isHTMXCall && component == componentServiceList && nextCursor != "": + resp, err := u.serviceApp.ListServices(ctx, app.ListServicesRequest{ + FilterSearchInput: data.ServiceSearchInput, + Cursor: nextCursor, + }) + if err != nil { + http.Error(w, "could not list services", http.StatusInternalServerError) + return + } + + data.Services = mapServiceToTPL(resp.Services) + data.ServicePagination = mapPaginationToTPL(resp.PaginationCursors, urls.URLWithComponent(currentURL, componentServiceList)) + + u.tplRenderer.RenderResponse(ctx, w, r, "app_services_comp_service_list", data) + + // Snippet service list previous. + case isHTMXCall && component == componentServiceList && prevCursor != "": + resp, err := u.serviceApp.ListServices(ctx, app.ListServicesRequest{ + FilterSearchInput: data.ServiceSearchInput, + Cursor: prevCursor, + }) + if err != nil { + http.Error(w, "could not list services", http.StatusInternalServerError) + return + } + + data.Services = mapServiceToTPL(resp.Services) + data.ServicePagination = mapPaginationToTPL(resp.PaginationCursors, urls.URLWithComponent(currentURL, componentServiceList)) + + u.tplRenderer.RenderResponse(ctx, w, r, "app_services_comp_service_list", data) + + // Snippet service list refresh snippet. + case isHTMXCall && component == componentServiceList: + resp, err := u.serviceApp.ListServices(ctx, app.ListServicesRequest{FilterSearchInput: data.ServiceSearchInput}) + if err != nil { + http.Error(w, "could not list services", http.StatusInternalServerError) + return + } + + data.Services = mapServiceToTPL(resp.Services) + data.ServicePagination = mapPaginationToTPL(resp.PaginationCursors, urls.URLWithComponent(currentURL, componentServiceList)) + + u.tplRenderer.RenderResponse(ctx, w, r, "app_services_comp_service_list", data) + + // Unknown snippet. + case isHTMXCall: + http.Error(w, "Unknown component", http.StatusBadRequest) + + // Full page load. + default: + resp, err := u.serviceApp.ListServices(ctx, app.ListServicesRequest{FilterSearchInput: data.ServiceSearchInput}) + if err != nil { + http.Error(w, "could not list services", http.StatusInternalServerError) + return + } + + data.Services = mapServiceToTPL(resp.Services) + data.ServicePagination = mapPaginationToTPL(resp.PaginationCursors, urls.URLWithComponent(currentURL, componentServiceList)) + + u.tplRenderer.RenderResponse(ctx, w, r, "app_services", data) + } + }) +} diff --git a/internal/http/ui/handler_select_service_test.go b/internal/http/ui/handler_select_service_test.go new file mode 100644 index 00000000..9a4544e5 --- /dev/null +++ b/internal/http/ui/handler_select_service_test.go @@ -0,0 +1,249 @@ +package ui_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/slok/sloth/internal/http/backend/app" + "github.com/slok/sloth/internal/http/backend/model" +) + +func TestHandlerSelectService(t *testing.T) { + tests := map[string]struct { + request func() *http.Request + mock func(m mocks) + expBody []string + expHeaders http.Header + expCode int + }{ + "Listing the services should render the full page (With services).": { + request: func() *http.Request { + return httptest.NewRequest(http.MethodGet, "/u/app/services", nil) + }, + mock: func(m mocks) { + expReq := app.ListServicesRequest{} + m.ServiceApp.On("ListServices", mock.Anything, expReq).Once().Return(&app.ListServicesResponse{ + PaginationCursors: app.PaginationCursors{ + PrevCursor: "test-prev-cursor", + NextCursor: "test-next-cursor", + HasNext: true, + HasPrevious: true, + }, + Services: []app.ServiceAlerts{ + { + Service: model.Service{ID: "test-svc1"}, + Alerts: []model.SLOAlerts{ + { + FiringPage: &model.Alert{Name: "page-1"}, + }, + { + FiringWarning: &model.Alert{Name: "warn-2"}, + }, + }, + }, + { + Service: model.Service{ID: "test-svc2"}, + Alerts: []model.SLOAlerts{ + { + FiringWarning: &model.Alert{Name: "warn-4"}, + }, + }, + }, + { + Service: model.Service{ID: "test-svc3"}, + Alerts: []model.SLOAlerts{}, + }, + }}, nil) + }, + expHeaders: http.Header{ + "Content-Type": {"text/html; charset=utf-8"}, + "Hx-Push-Url": {"/u/app/services"}, + }, + expCode: 200, + expBody: []string{ + ``, // We rendered a full page. + `