Compare commits

...

56 Commits

Author SHA1 Message Date
Stefan Prodan 27daa2ca46
Merge pull request #1803 from alex-souslik-hs/main
loadtester: add pod security context
2025-04-21 09:23:30 +02:00
Alex ed38a79545 add pod security context
Signed-off-by: Alex <alex.souslik@workday.com>
2025-04-20 19:15:37 +03:00
Sanskar Jaiswal 6f165a10de
Merge pull request #1788 from fluxcd/release-v1.41.0
Release v1.41.0
2025-04-02 12:40:43 +01:00
Sanskar Jaiswal 89c1ddee79
Release v1.41.0
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
2025-04-02 12:32:23 +01:00
Sanskar Jaiswal 1b8e7653d3
Merge pull request #1787 from fluxcd/update-deps
update Go dependencies
2025-03-31 18:09:11 +05:30
Stefan Prodan 98f8514258
Merge pull request #1786 from fluxcd/fix-session-affinity-e2e
update webhook host in session affinity e2e test
2025-03-30 14:04:02 +01:00
Sanskar Jaiswal d9c8a09d3e
update Go dependencies
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
2025-03-29 18:00:26 +00:00
Sanskar Jaiswal 2ac22f831f
update webhook host in session affinity e2e test
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
2025-03-28 12:30:23 +05:30
Sanskar Jaiswal e0de40dcb0
Merge pull request #1757 from otternq/metric-provider-headers
allow headers to be added to prometheus requests
2025-03-28 12:26:38 +05:30
Nick Otter 8f9bb5b1bc
allow headers to be added to prometheus requests
Signed-off-by: Nick Otter <otternq@gmail.com>
Co-authored-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
2025-03-27 01:03:23 +05:30
Sanskar Jaiswal f21bc1de3e
Merge pull request #1783 from fluxcd/session-affinity-primary-cookie
feat: add support for primary backend cookies in session affinity (Gateway API)
2025-03-24 16:02:30 +05:30
Sanskar Jaiswal 1fc7ac5847
add docs for primary stickiness for session affinity
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>

dfk
2025-03-24 13:08:29 +05:30
Sanskar Jaiswal 1dc270c2e6
feat: add support for primary backend cookies in session affinity
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
2025-03-24 13:08:27 +05:30
Stefan Prodan 50d1331ba6
Merge pull request #1785 from fluxcd/loadtester-0.35.0
Release loadtester 0.35.0
2025-03-23 10:14:29 +02:00
Stefan Prodan bc78156535
Release loadtester 0.35.0
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2025-03-23 09:31:44 +02:00
Stefan Prodan 0df8af8d04
Merge pull request #1771 from fluxcd/dependabot/github_actions/ci-8077bd6f50
build(deps): bump the ci group across 1 directory with 2 updates
2025-03-23 09:29:16 +02:00
dependabot[bot] 633f639383
build(deps): bump the ci group across 1 directory with 2 updates
Bumps the ci group with 2 updates in the / directory: [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) and [slsa-framework/slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator).


Updates `sigstore/cosign-installer` from 3.7.0 to 3.8.1
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.7.0...v3.8.1)

Updates `slsa-framework/slsa-github-generator` from 2.0.0 to 2.1.0
- [Release notes](https://github.com/slsa-framework/slsa-github-generator/releases)
- [Changelog](https://github.com/slsa-framework/slsa-github-generator/blob/main/CHANGELOG.md)
- [Commits](https://github.com/slsa-framework/slsa-github-generator/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
- dependency-name: slsa-framework/slsa-github-generator
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-23 07:19:45 +00:00
Stefan Prodan d03cc73386
Merge pull request #1784 from fluxcd/go-1.24
Build with Go 1.24
2025-03-23 09:17:55 +02:00
Stefan Prodan eaf5bb992c
Ensure constant format strings in fmt calls
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2025-03-23 08:59:45 +02:00
Stefan Prodan 22618ccb11
Build with Go 1.24
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2025-03-23 08:50:59 +02:00
Stefan Prodan f5af225ffc
Merge pull request #1776 from fluxcd/dependabot/go_modules/golang.org/x/net-0.36.0
build(deps): bump golang.org/x/net from 0.33.0 to 0.36.0
2025-03-23 08:45:20 +02:00
dependabot[bot] 40a34199fe
build(deps): bump golang.org/x/net from 0.33.0 to 0.36.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-23 06:40:43 +00:00
Stefan Prodan d7357a7377
Merge pull request #1682 from tombanksme/knative-support
Add support for Knative
2025-03-23 08:39:29 +02:00
Sanskar Jaiswal 12ee6cbc86
add docs for knative
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
2025-03-22 01:02:32 +05:30
Thomas Banks f1c8807c0d
feat: add knative integration
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
Co-authored-by: Thomas Banks
2025-03-22 01:02:30 +05:30
Stefan Prodan 8276bfa5a5
Merge pull request #1763 from easimon/fix/datadog-provider
fix: do not evaluate incomplete samples from datadog
2025-02-22 10:36:22 +02:00
Markus Dobel 2c4b7a69a2 fix: do not evaluate incomplete samples from datadog
Signed-off-by: Markus Dobel <markus.dobel@epicompany.eu>
2025-02-12 18:12:38 +01:00
Sanskar Jaiswal 660ed7486b
Merge pull request #1677 from jdgeisler/keda-scaled-object-hpa-migration
Prevent primary hpa collision for keda scaled objects when migrating from an hpa
2025-02-11 22:49:54 +05:30
James Geisler 21acd7e3d6 If applied, this commit will allow the migration from an hpa to a scaled object
Signed-off-by: James Geisler <geislerjamesd@gmail.com>
2025-02-10 10:24:58 -06:00
Stefan Prodan 40e2802c3d
Merge pull request #1707 from quintonm/main
chart: add support for deploymentLabels
2025-01-26 09:44:37 +02:00
Sanskar Jaiswal d99d37b219
Merge pull request #1755 from fluxcd/headless-svc 2025-01-14 15:02:59 +05:30
Sanskar Jaiswal 45618b90db
feat: add option to generate headless services
Add a new field `.spec.service.headless` which if set to true results in
Flagger generating headless Services, i.e. with the Service's
`.spec.clusterIP` set to None.

Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
2025-01-14 14:09:12 +05:30
Sanskar Jaiswal ff4051f728
Merge pull request #1756 from fluxcd/bump-go-net
chore: bump golang.org/x/net to v0.33.0
2025-01-14 13:18:03 +05:30
Sanskar Jaiswal 2ea13a477b
chore: bump golang.org/x/net to v0.33.0
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
2025-01-14 00:18:27 +05:30
quintonm 03d4acc77f add support for deploymentLabels
Signed-off-by: quintonm <quinton.mccombs@gmail.com>
2025-01-13 07:56:30 -06:00
Stefan Prodan 16a607549e
Merge pull request #1751 from fluxcd/dependabot/github_actions/ci-cba3cefdba
Bump helm/kind-action from 1.11.0 to 1.12.0 in the ci group
2024-12-23 14:12:52 +02:00
dependabot[bot] b57afd3b0f
Bump helm/kind-action from 1.11.0 to 1.12.0 in the ci group
Bumps the ci group with 1 update: [helm/kind-action](https://github.com/helm/kind-action).


Updates `helm/kind-action` from 1.11.0 to 1.12.0
- [Release notes](https://github.com/helm/kind-action/releases)
- [Commits](https://github.com/helm/kind-action/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: helm/kind-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 11:28:50 +00:00
Stefan Prodan 9000136233
Merge pull request #1749 from fluxcd/release-1.40.0
Release v1.40.0
2024-12-17 11:42:02 +02:00
Stefan Prodan 14543cc8bf
Release v1.40.0
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2024-12-17 11:14:49 +02:00
Stefan Prodan 642ef6bb7d
Merge pull request #1745 from fluxcd/dependabot/github_actions/ci-f4c9def711
Bump helm/kind-action from 1.10.0 to 1.11.0 in the ci group
2024-12-17 10:06:24 +02:00
Stefan Prodan 3ebbfb0a54
Merge pull request #1747 from fluxcd/loadtester-0.34.0
Release loadtester 0.34.0
2024-12-17 10:05:54 +02:00
Stefan Prodan a52f497370
Release loadtester 0.34.0
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2024-12-16 19:27:19 +02:00
Stefan Prodan 64b50813ff
Merge pull request #1744 from fluxcd/update-deps-alpine
Update dependencies
2024-12-16 19:09:11 +02:00
Stefan Prodan 9244d6de65
Merge pull request #1746 from fluxcd/fix-drift-aws-gateway
Preserve HTTPRoute annotations injected by AWS Gateway API
2024-12-16 19:08:56 +02:00
Sanskar Jaiswal 3b6b550d64
Add tests for annotations preservation in Gateway API router
Signed-off-by: Sanskar Jaiswal <jaiswalsanskar078@gmail.com>
2024-12-16 22:16:08 +05:30
Stefan Prodan 282f2b36f0
Preserve HTTPRoute annotations injected by AWS Gateway API
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2024-12-16 16:35:41 +02:00
dependabot[bot] 0a76f808b8
Bump helm/kind-action from 1.10.0 to 1.11.0 in the ci group
Bumps the ci group with 1 update: [helm/kind-action](https://github.com/helm/kind-action).


Updates `helm/kind-action` from 1.10.0 to 1.11.0
- [Release notes](https://github.com/helm/kind-action/releases)
- [Commits](https://github.com/helm/kind-action/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: helm/kind-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 11:48:55 +00:00
Stefan Prodan a85887de3c
Merge pull request #1735 from kahirokunn/add-helper-gen
Automate zz_generated.deepcopy.go updates with make codegen
2024-12-13 18:52:29 +02:00
kahirokunn febc327673
chore(codegen): add helper generation to codegen script
chore(gatewayapi/v1beta1): add deepcopy-gen annotations
run `make codegen`

Signed-off-by: kahirokunn <okinakahiro@gmail.com>
2024-12-14 00:10:24 +09:00
Stefan Prodan 6d5aabff05
Update loadtester tools
- helm 3.16.3
- kubectl 1.31.3
- grcp probe 0.4.35
- bash 5.2.37
- bats 1.1.1

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2024-12-13 16:23:12 +02:00
Stefan Prodan 51d0bb2c92
Update Alpine to 3.21
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2024-12-13 16:21:33 +02:00
Stefan Prodan dc947fb164
Update Go dependencies
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2024-12-13 16:21:12 +02:00
Stefan Prodan 0138e2e6c4
Merge pull request #1733 from kane8n/add-splunk-provider
Add Splunk as a metrics provider
2024-12-13 16:11:25 +02:00
kane8n d4bd0f2ef8 add splunk provider
Signed-off-by: kane8n <takumi.kaneda@zozo.com>
2024-12-13 22:22:25 +09:00
Stefan Prodan 30f4b25925
Merge pull request #1731 from fluxcd/fix-changelog-date
Fix changelog date for 1.39 release
2024-11-26 12:38:33 +00:00
Stefan Prodan 25fd9be1db
Fix changelog date for 1.39 release
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2024-11-26 12:12:17 +00:00
93 changed files with 3095 additions and 464 deletions

View File

@ -17,3 +17,4 @@ redirects:
usage/kuma-progressive-delivery: tutorials/kuma-progressive-delivery.md
usage/gatewayapi-progressive-delivery: tutorials/gatewayapi-progressive-delivery.md
usage/apisix-progressive-delivery: tutorials/apisix-progressive-delivery.md
usage/knative-progressive-delivery: tutorials/knative-progressive-delivery.md

View File

@ -23,7 +23,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.23.x
go-version: 1.24.x
cache-dependency-path: |
**/go.sum
**/go.mod

View File

@ -35,18 +35,19 @@ jobs:
- gatewayapi
- keda
- apisix
- knative
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Kubernetes
uses: helm/kind-action@v1.10.0
uses: helm/kind-action@v1.12.0
if: matrix.provider != 'skipper'
with:
version: v0.23.0
cluster_name: kind
node_image: kindest/node:v1.30.0@sha256:047357ac0cfea04663786a612ba1eaba9702bef25227a794b52890dd8bcd692e
- name: Setup Kubernetes for skipper
uses: helm/kind-action@v1.10.0
uses: helm/kind-action@v1.12.0
if: matrix.provider == 'skipper'
with:
version: v0.23.0

View File

@ -17,7 +17,7 @@ jobs:
packages: write
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3.7.0
- uses: sigstore/cosign-installer@v3.8.1
- name: Prepare
id: prep
run: |

View File

@ -31,9 +31,9 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.23.x
go-version: 1.24.x
- uses: fluxcd/flux2/action@main
- uses: sigstore/cosign-installer@v3.7.0
- uses: sigstore/cosign-installer@v3.8.1
- name: Prepare
id: prep
run: |
@ -146,7 +146,7 @@ jobs:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
contents: write # for uploading attestations to GitHub releases.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
provenance-name: "provenance.intoto.jsonl"
base64-subjects: "${{ needs.release-flagger.outputs.hashes }}"

View File

@ -34,7 +34,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.23.x
go-version: 1.24.x
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:

View File

@ -2,9 +2,80 @@
All notable changes to this project are documented in this file.
## 1.41.0
**Release date:** 2025-04-02
This release comes with major features and minor bug fixes.
Flagger now supports Knative as a networking provider. This works a bit
differently than compared to other service meshes/ingresses. Flagger does not
generate any Kubernetes objects. It instead modifies the Knative service itself
to configure weighted traffic routing. To learn more, please see the [tutorial](https://docs.flagger.app/tutorials/knative-progressive-delivery).
The session affinity canary release strategy has also been improved. Flagger can
now configure Gateway API HTTPRoutes to also set a cookie for the primary
deployment's response. For more info, see the [strategy docs](https://docs.flagger.app/usage/deployment-strategies#canary-release-with-session-affinity).
Furthermore, there's a new `.spec.service.headless` field which when set to
true, tells Flagger to generate headless Kubernetes services. Also, support has
been added for adding headers to the request Flagger sends to Prometheus for
collecting metrics during an analysis via the `.spec.headers` field in the
`MetricTemplate` object.
Finally, both Flagger and the load tester have been updated to use Go 1.24 and
their dependencies have been updated as well.
#### Improvements
- Allow headers to be added to Prometheus requests
[#1757](https://github.com/fluxcd/flagger/pull/1757)
- feat: Add support for primary backend cookies in session affinity (Gateway API)
[#1783](https://github.com/fluxcd/flagger/pull/1783)
- Update Go dependencies
[#1787](https://github.com/fluxcd/flagger/pull/1787)
- Build with Go 1.24
[#1784](https://github.com/fluxcd/flagger/pull/1784)
- Add support for Knative
[#1682](https://github.com/fluxcd/flagger/pull/1682)
- chart: add support for deploymentLabels
[#1707](https://github.com/fluxcd/flagger/pull/1707)
- chart: add support for deploymentLabels
[#1707](https://github.com/fluxcd/flagger/pull/1707)
- feat: add option to generate headless services
[#1755](https://github.com/fluxcd/flagger/pull/1755)
#### Fixes
- Fix: Do not evaluate incomplete samples from Datadog
[#1763](https://github.com/fluxcd/flagger/pull/1763)
- Prevent primary HPA collision for KEDA scaled objects when migrating from an HPA
[#1677](https://github.com/fluxcd/flagger/pull/1677)
## 1.40.0
**Release date:** 2024-12-17
This release comes with support for Splunk Observability (formerly SignalFx) as a metrics provider.
For more information on how to write `MetricTemplates` for Splunk, please see the
[Splunk metrics tutorial](https://docs.flagger.app/usage/metrics#s#splunk).
Starting with this version, Flagger is compatible with the
[AWS Gateway API Controller](https://www.gateway-api-controller.eks.aws.dev/latest/).
Both Flagger and the load tester Go dependencies have been updated to fix various CVEs.
#### Improvements
- Add Splunk as a metrics provider
[#1733](https://github.com/fluxcd/flagger/pull/1733)
- Preserve HTTPRoute annotations injected by AWS Gateway API
[#1746](https://github.com/fluxcd/flagger/pull/1746)
- Automate `zz_generated.deepcopy.go` updates with make codegen
[#1735](https://github.com/fluxcd/flagger/pull/1735)
- Update dependencies
[#1744](https://github.com/fluxcd/flagger/pull/1744)
## 1.39.0
**Release date:** 2024-07-30
**Release date:** 2024-11-26
This release comes with fixes and improvements. There is a new
`.spec.analysis.webhooks[].disableTLS` field which disables TLS verification
@ -503,7 +574,7 @@ routed to the canary workload pods.
**Release date:** 2022-12-15
This release comes with support for Apachae APISIX. For more details see the
This release comes with support for Apache APISIX. For more details see the
[tutorial](https://fluxcd.io/flagger/tutorials/apisix-progressive-delivery).
#### Improvements

View File

@ -1,8 +1,8 @@
ARG GO_VERSION=1.23
ARG XX_VERSION=1.4.0
ARG GO_VERSION=1.24
ARG XX_VERSION=1.6.1
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine as builder
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS builder
# copy build utilities
COPY --from=xx / /
@ -29,7 +29,7 @@ RUN xx-go build \
-ldflags "-s -w -X github.com/fluxcd/flagger/pkg/version.REVISION=${REVISON}" \
-a -o flagger ./cmd/flagger
FROM alpine:3.20
FROM alpine:3.21
RUN apk --no-cache add ca-certificates

View File

@ -1,4 +1,4 @@
FROM golang:1.23-alpine as builder
FROM golang:1.24-alpine AS builder
ARG TARGETPLATFORM
ARG TARGETARCH
@ -6,15 +6,15 @@ ARG REVISION
RUN apk --no-cache add alpine-sdk perl curl bash tar
RUN HELM3_VERSION=3.15.3 && \
RUN HELM3_VERSION=3.17.2 && \
curl -sSL "https://get.helm.sh/helm-v${HELM3_VERSION}-linux-${TARGETARCH}.tar.gz" | tar xvz && \
chmod +x linux-${TARGETARCH}/helm && mv linux-${TARGETARCH}/helm /usr/local/bin/helm
RUN KUBECTL_VERSION=v1.29.7 && \
RUN KUBECTL_VERSION=v1.31.3 && \
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${TARGETARCH}/kubectl" && \
chmod +x kubectl && mv kubectl /usr/local/bin/kubectl
RUN GRPC_HEALTH_PROBE_VERSION=v0.4.28 && \
RUN GRPC_HEALTH_PROBE_VERSION=v0.4.35 && \
wget -qO /usr/local/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-${TARGETARCH} && \
chmod +x /usr/local/bin/grpc_health_probe
@ -39,7 +39,7 @@ COPY pkg/ pkg/
# build
RUN CGO_ENABLED=0 go build -o loadtester ./cmd/loadtester/*
FROM bash:5.0
FROM bash:5.2
ARG TARGETPLATFORM
@ -49,7 +49,7 @@ apk --no-cache add ca-certificates curl jq libgcc wrk hey git
WORKDIR /home/app
COPY --from=bats/bats:v1.1.0 /opt/bats/ /opt/bats/
COPY --from=bats/bats:1.11.1 /opt/bats/ /opt/bats/
RUN ln -s /opt/bats/bin/bats /usr/local/bin/
COPY --from=builder /usr/local/bin/helm /usr/local/bin/

View File

@ -6,7 +6,7 @@ build:
CGO_ENABLED=0 go build -a -o ./bin/flagger ./cmd/flagger
tidy:
rm -f go.sum; go mod tidy -compat=1.23
rm -f go.sum; go mod tidy -compat=1.24
vet:
go vet ./...

View File

@ -1,4 +1,4 @@
# flagger
# flaggerreadme
[![release](https://img.shields.io/github/release/fluxcd/flagger/all.svg)](https://github.com/fluxcd/flagger/releases)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4783/badge)](https://bestpractices.coreinfrastructure.org/projects/4783)
@ -184,17 +184,17 @@ For more details on how the canary analysis and promotion works please [read the
**Service Mesh**
| Feature | App Mesh | Istio | Linkerd | Kuma | OSM | Kubernetes CNI |
|--------------------------------------------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------|
| Canary deployments (weighted traffic) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: |
| A/B testing (headers and cookies routing) | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_minus_sign: | :heavy_minus_sign: | :heavy_minus_sign: |
| Blue/Green deployments (traffic switch) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Blue/Green deployments (traffic mirroring) | :heavy_minus_sign: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_minus_sign: | :heavy_minus_sign: | :heavy_minus_sign: |
| Webhooks (acceptance/load testing) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Manual gating (approve/pause/resume) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Request success rate check (L7 metric) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: |
| Request duration check (L7 metric) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: |
| Custom metric checks | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Feature | App Mesh | Istio | Linkerd | Kuma | OSM | Knative | Kubernetes CNI |
|--------------------------------------------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------|--------------------|
| Canary deployments (weighted traffic) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: |
| A/B testing (headers and cookies routing) | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_minus_sign: | :heavy_minus_sign: | :heavy_minus_sign: | :heavy_minus_sign: |
| Blue/Green deployments (traffic switch) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_check_mark: |
| Blue/Green deployments (traffic mirroring) | :heavy_minus_sign: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_minus_sign: | :heavy_minus_sign: | :heavy_minus_sign: | :heavy_minus_sign: |
| Webhooks (acceptance/load testing) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Manual gating (approve/pause/resume) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
| Request success rate check (L7 metric) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_check_mark: |
| Request duration check (L7 metric) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_minus_sign: | :heavy_check_mark: |
| Custom metric checks | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
**Ingress**
@ -243,7 +243,6 @@ can be used to implement the request success rate and request duration checks.
#### Integrations
- Add support for ingress controllers like HAProxy, ALB, and Apache APISIX
- Add support for Knative Serving
### Contributing

View File

@ -80,7 +80,6 @@ spec:
type: object
required:
- targetRef
- service
- analysis
properties:
provider:
@ -198,6 +197,9 @@ spec:
portDiscovery:
description: Enable port dicovery
type: boolean
headless:
description: Headless if set to true, generates headless Kubernetes services.
type: boolean
timeout:
description: HTTP or gRPC request timeout
type: string
@ -1153,6 +1155,9 @@ spec:
cookieName:
description: CookieName is the key that will be used for the session affinity cookie.
type: string
primaryCookieName:
description: CookieName is the key that will be used for the session affinity cookie.
type: string
maxAge:
description: MaxAge indicates the number of seconds until the session affinity cookie will expire.
default: 86400
@ -1304,9 +1309,17 @@ spec:
- graphite
- dynatrace
- keptn
- splunk
address:
description: API address of this provider
type: string
headers:
description: Headers to add to HTTP(S) requests
type: object
additionalProperties:
type: array
items:
type: string
secretRef:
description: Kubernetes secret reference containing the provider credentials
type: object

View File

@ -22,7 +22,7 @@ spec:
serviceAccountName: flagger
containers:
- name: flagger
image: ghcr.io/fluxcd/flagger:1.39.0
image: ghcr.io/fluxcd/flagger:1.41.0
imagePullPolicy: IfNotPresent
ports:
- name: http

View File

@ -1,7 +1,7 @@
apiVersion: v1
name: flagger
version: 1.39.0
appVersion: 1.39.0
version: 1.41.0
appVersion: 1.41.0
kubeVersion: ">=1.19.0-0"
engine: gotpl
description: Flagger is a progressive delivery operator for Kubernetes

View File

@ -114,6 +114,15 @@ $ helm upgrade -i flagger flagger/flagger \
--set meshProvider=traefik
```
If you need to add labels to the flagger deployment or pods, you can pass the labels as parameters as shown below.
```console
helm upgrade -i flagger flagger/flagger \
<other parameters> \
--set podLabels.<labelName>=<labelValue> \
--set deploymentLabels.<labelName>=<labelValue>
```
The [configuration](#configuration) section lists the parameters that can be configured during installation.
## Uninstalling the Chart
@ -186,6 +195,8 @@ The following tables lists the configurable parameters of the Flagger chart and
| `podDisruptionBudget.minAvailable` | The minimal number of available replicas that will be set in the PodDisruptionBudget | `1` |
| `noCrossNamespaceRefs` | If `true`, cross namespace references to custom resources will be disabled | `false` |
| `namespace` | When specified, Flagger will restrict itself to watching Canary objects from that namespace | `""` |
| `deploymentLabels` | Labels to add to Flagger deployment | `{}` |
| `podLabels` | Labels to add to pods of Flagger deployment | `{}` |
Specify each parameter using the `--set key=value[,key=value]` argument to `helm upgrade`. For example,

View File

@ -80,7 +80,6 @@ spec:
type: object
required:
- targetRef
- service
- analysis
properties:
provider:
@ -198,6 +197,9 @@ spec:
portDiscovery:
description: Enable port dicovery
type: boolean
headless:
description: Headless if set to true, generates headless Kubernetes services.
type: boolean
timeout:
description: HTTP or gRPC request timeout
type: string
@ -1153,6 +1155,9 @@ spec:
cookieName:
description: CookieName is the key that will be used for the session affinity cookie.
type: string
primaryCookieName:
description: CookieName is the key that will be used for the session affinity cookie.
type: string
maxAge:
description: MaxAge indicates the number of seconds until the session affinity cookie will expire.
default: 86400
@ -1304,9 +1309,17 @@ spec:
- graphite
- dynatrace
- keptn
- splunk
address:
description: API address of this provider
type: string
headers:
description: Headers to add to HTTP(S) requests
type: object
additionalProperties:
type: array
items:
type: string
secretRef:
description: Kubernetes secret reference containing the provider credentials
type: object

View File

@ -9,6 +9,11 @@ metadata:
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
{{- if .Values.deploymentLabels }}
{{- range $key, $value := .Values.deploymentLabels }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
{{- with .Values.annotations }}
annotations:
{{- toYaml . | nindent 4 }}

View File

@ -276,6 +276,19 @@ rules:
- /version
verbs:
- get
- apiGroups:
- serving.knative.dev
resources:
- services
verbs:
- get
- update
- apiGroups:
- serving.knative.dev
resources:
- revisions
verbs:
- get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@ -5,7 +5,7 @@
image:
repository: ghcr.io/fluxcd/flagger
tag: 1.39.0
tag: 1.41.0
pullPolicy: IfNotPresent
pullSecret:
@ -195,8 +195,12 @@ podDisruptionBudget:
enabled: false
minAvailable: 1
# Additional labels to be added to pods
podLabels: {}
# Additional labels to be added to deployments
deploymentLabels: { }
noCrossNamespaceRefs: false
#Placeholder to supply additional volumes to the flagger pod

View File

@ -1,7 +1,7 @@
apiVersion: v1
name: loadtester
version: 0.33.0
appVersion: 0.33.0
version: 0.35.0
appVersion: 0.35.0
kubeVersion: ">=1.19.0-0"
engine: gotpl
description: Flagger's load testing services based on rakyll/hey and bojand/ghz that generates traffic during canary analysis when configured as a webhook.

View File

@ -69,8 +69,10 @@ The following tables lists the configurable parameters of the load tester chart
| `istio.tls.enabled` | Enable TLS in gateway ( TLS secrets should be in namespace ) | `false` |
| `istio.tls.httpsRedirect` | Redirect traffic to TLS port | `false` |
| `podPriorityClassName` | PriorityClass name for pod priority configuration | "" |
| `securityContext.enabled` | Add securityContext to container | "" |
| `securityContext.context` | securityContext to add | "" |
| `securityContext.enabled` | Add securityContext to container | `false` |
| `SecurityContext.context` | securityContext to add | "" |
| `podSecurityContext.enabled` | Add securityContext to pod | `false` |
| `podSecurityContext.context` | securityContext to add | "" |
| `podDisruptionBudget.enabled` | A PodDisruptionBudget will be created if `true` | `false` |
| `podDisruptionBudget.minAvailable` | The minimal number of available replicas that will be set in the PodDisruptionBudget | `1` |

View File

@ -24,7 +24,7 @@ spec:
appmesh.k8s.aws/ports: "444"
openservicemesh.io/inbound-port-exclusion-list: "80, 8080"
{{- if .Values.podAnnotations }}
{{ toYaml .Values.podAnnotations | indent 8 }}
{{- toYaml .Values.podAnnotations | nindent 8 }}
{{- end }}
spec:
{{- if .Values.serviceAccountName }}
@ -39,7 +39,7 @@ spec:
- name: {{ .Chart.Name }}
{{- if .Values.securityContext.enabled }}
securityContext:
{{ toYaml .Values.securityContext.context | indent 12 }}
{{- toYaml .Values.securityContext.context | nindent 12 }}
{{- end }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
@ -102,3 +102,7 @@ spec:
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.podSecurityContext.enabled }}
securityContext:
{{- toYaml .Values.podSecurityContext.context | nindent 12 }}
{{- end }}

View File

@ -2,7 +2,7 @@ replicaCount: 1
image:
repository: ghcr.io/fluxcd/flagger-loadtester
tag: 0.33.0
tag: 0.35.0
pullPolicy: IfNotPresent
pullSecret:
@ -91,6 +91,12 @@ securityContext:
runAsUser: 100
runAsGroup: 101
podSecurityContext:
enabled: false
context:
fsGroup: 101
fsGroupChangePolicy: "OnRootMismatch"
podDisruptionBudget:
enabled: false
minAvailable: 1

View File

@ -51,6 +51,8 @@ import (
"github.com/fluxcd/flagger/pkg/server"
"github.com/fluxcd/flagger/pkg/signals"
"github.com/fluxcd/flagger/pkg/version"
knative "knative.dev/serving/pkg/client/clientset/versioned"
)
var (
@ -110,7 +112,7 @@ func init() {
flag.BoolVar(&zapReplaceGlobals, "zap-replace-globals", false, "Whether to change the logging level of the global zap logger.")
flag.StringVar(&zapEncoding, "zap-encoding", "json", "Zap logger encoding.")
flag.StringVar(&namespace, "namespace", "", "Namespace that flagger would watch canary object.")
flag.StringVar(&meshProvider, "mesh-provider", "istio", "Service mesh provider, can be istio, linkerd, appmesh, contour, gloo, nginx, skipper, traefik, apisix, osm or kuma.")
flag.StringVar(&meshProvider, "mesh-provider", "istio", "Service mesh provider, can be istio, linkerd, appmesh, contour, knative, gloo, nginx, skipper, traefik, apisix, osm or kuma.")
flag.StringVar(&selectorLabels, "selector-labels", "app,name,app.kubernetes.io/name", "List of pod labels that Flagger uses to create pod selectors.")
flag.StringVar(&ingressAnnotationsPrefix, "ingress-annotations-prefix", "nginx.ingress.kubernetes.io", "Annotations prefix for NGINX ingresses.")
flag.StringVar(&ingressClass, "ingress-class", "", "Ingress class used for annotating HTTPProxy objects.")
@ -166,6 +168,11 @@ func main() {
logger.Fatalf("Error building flagger clientset: %s", err.Error())
}
knativeClient, err := knative.NewForConfig(cfg)
if err != nil {
logger.Fatalf("Error building knative clientset: %s", err.Error())
}
// use a remote cluster for routing if a service mesh kubeconfig is specified
if kubeconfigServiceMesh == "" {
kubeconfigServiceMesh = kubeconfig
@ -221,7 +228,7 @@ func main() {
setOwnerRefs = false
}
routerFactory := router.NewFactory(cfg, kubeClient, flaggerClient, ingressAnnotationsPrefix, ingressClass, logger, meshClient, setOwnerRefs)
routerFactory := router.NewFactory(cfg, kubeClient, flaggerClient, knativeClient, ingressAnnotationsPrefix, ingressClass, logger, meshClient, setOwnerRefs)
var configTracker canary.Tracker
if enableConfigTracking {
@ -236,10 +243,11 @@ func main() {
includeLabelPrefixArray := strings.Split(includeLabelPrefix, ",")
canaryFactory := canary.NewFactory(kubeClient, flaggerClient, configTracker, labels, includeLabelPrefixArray, logger)
canaryFactory := canary.NewFactory(kubeClient, flaggerClient, knativeClient, configTracker, labels, includeLabelPrefixArray, logger)
c := controller.NewController(
kubeClient,
knativeClient,
flaggerClient,
infos,
controlLoopInterval,

View File

@ -29,7 +29,7 @@ import (
"github.com/fluxcd/flagger/pkg/signals"
)
var VERSION = "0.33.0"
var VERSION = "0.35.0"
var (
logLevel string
port string

View File

@ -35,6 +35,7 @@
* [Open Service Mesh Deployments](tutorials/osm-progressive-delivery.md)
* [Kuma Canary Deployments](tutorials/kuma-progressive-delivery.md)
* [Gateway API Canary Deployments](tutorials/gatewayapi-progressive-delivery.md)
* [Knative Canary Deployments](tutorials/knative-progressive-delivery.md)
* [Blue/Green Deployments](tutorials/kubernetes-blue-green.md)
* [Canary analysis with Prometheus Operator](tutorials/prometheus-operator.md)
* [Canary analysis with KEDA ScaledObjects](tutorials/keda-scaledobject.md)

View File

@ -81,6 +81,15 @@ $ helm upgrade -i flagger flagger/flagger \
--set metricsServer=http://osm-prometheus.osm-system.svc:7070
```
If you need to add labels to the flagger deployment or pods, you can pass the labels as parameters as shown below.
```console
helm upgrade -i flagger flagger/flagger \
<other parameters> \
--set podLabels.<labelName>=<labelValue> \
--set deploymentLabels.<labelName>=<labelValue>
```
You can install Flagger in any namespace as long as it can talk to the Prometheus service on port 9090.
For ingress controllers, the install instructions are:

View File

@ -509,6 +509,9 @@ You can load `www.example.com` in your browser and refresh it until you see the
All subsequent requests after that will be served by `podinfo:6.0.1` and not `podinfo:6.0.0` because of the session affinity
configured by Flagger in the HTTPRoute object.
To configure stickiness for the Primary deployment to ensure fair weighted traffic routing, please
checkout the [deployment strategies docs](../usage/deployment-strategies.md#canary-release-with-session-affinity).
# A/B Testing
Besides weighted routing, Flagger can be configured to route traffic to the canary based on HTTP match conditions. In an A/B testing scenario, you'll be using HTTP headers or cookies to target a certain segment of your users. This is particularly useful for frontend applications that require session affinity.

View File

@ -0,0 +1,249 @@
# Knative Canary Deployments
This guide shows you how to use [Knative](https://knative.dev/) and Flagger to automate canary deployments.
![Flagger Canary Stages](https://raw.githubusercontent.com/fluxcd/flagger/main/docs/diagrams/flagger-gatewayapi-canary.png)
## Prerequisites
Flagger requires a Kubernetes cluster **v1.19** or newer and a Knative Serving installation that supports
the resources with `serving.knative.dev/v1` as their API version.
Install Knative v1.17.0:
```bash
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.17.0/serving-crds.yaml
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.17.0/serving-core.yaml
kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v1.17.0/kourier.yaml
kubectl patch configmap/config-network \
--namespace knative-serving \
--type merge \
--patch '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}'
```
Install Flagger in the `flagger-system` namespace:
```bash
kubectl apply -k github.com/fluxcd/flagger//kustomize/knative
```
Create a namespace for your Kntive Service:
```bash
kubectl create namespace test
```
Create a Knative Service that deploys podinfo:
```yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: podinfo
namespace: test
spec:
template:
spec:
containers:
- image: ghcr.io/stefanprodan/podinfo:6.0.0
ports:
- containerPort: 9898
protocol: TCP
command:
- ./podinfo
- --port=9898
- --port-metrics=9797
- --grpc-port=9999
- --grpc-service-name=podinfo
- --level=info
- --random-delay=false
- --random-error=false
```
Deploy the load testing service to generate traffic during the canary analysis:
```bash
kubectl apply -k https://github.com/fluxcd/flagger//kustomize/tester?ref=main
```
Create a Canary custom resource:
```yaml
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: podinfo
namespace: test
spec:
provider: knative
# knative service ref
targetRef:
apiVersion: serving.knative.dev/v1
kind: Service
name: podinfo
# the maximum time in seconds for the canary deployment
# to make progress before it is rollback (default 600s)
progressDeadlineSeconds: 60
analysis:
# schedule interval (default 60s)
interval: 15s
# max number of failed metric checks before rollback
threshold: 15
# max traffic percentage routed to canary
maxWeight: 50
# canary increment step
# percentage (0-100)
stepWeight: 10
metrics:
- name: request-success-rate
# min success rate (non-5xx responses)
# percentage (0-100)
thresholdRange:
min: 99
interval: 1m
- name: request-duration
# milliseconds
thresholdRange:
max: 500
interval: 1m
webhooks:
- name: load-test
url: http://flagger-loadtester.test/
timeout: 5s
metadata:
type: cmd
cmd: "hey -z 1m -q 5 -c 2 http://podinfo.test"
logCmdOutput: "true"
```
> Note: Please note that for a Canary resource with `.spec.provider` set to `knative`, the resource is only valid if the
`.spec.targetRef.kind` is `Service` and `.spec.targetRef.apiVersion` is `serving.knative.dev/v1`.
Save the above resource as podinfo-canary.yaml and then apply it:
```bash
kubectl apply -f ./podinfo-canary.yaml
```
When the canary analysis starts, Flagger will call the pre-rollout webhooks before routing traffic to the canary.
The canary analysis will run for five minutes while validating the HTTP metrics and rollout hooks every minute.
After a couple of seconds Flagger will make the following changes the Knative Service `podinfo`:
* Add an annotation to the object with the name `flagger.app/primary-revision`.
* Modify the `.spec.traffic` section of the object such that it can manipulate the traffic spread between
the primary and canary Knative Revision.
## Automated canary promotion
Trigger a canary deployment by updating the container image:
```bash
kubectl -n test set image deployment/podinfo \
podinfod=stefanprodan/podinfo:6.0.1
```
Flagger detects that the deployment revision changed and starts a new rollout:
```text
kubectl -n test describe canary/podinfo
Status:
Canary Weight: 0
Failed Checks: 0
Phase: Succeeded
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Synced 3m flagger New revision detected podinfo.test
Normal Synced 3m flagger Scaling up podinfo.test
Normal Synced 3m flagger Advance podinfo.test canary weight 5
Normal Synced 3m flagger Advance podinfo.test canary weight 10
Normal Synced 3m flagger Advance podinfo.test canary weight 15
Normal Synced 2m flagger Advance podinfo.test canary weight 20
Normal Synced 2m flagger Advance podinfo.test canary weight 25
Normal Synced 1m flagger Advance podinfo.test canary weight 30
Normal Synced 1m flagger Advance podinfo.test canary weight 35
Normal Synced 55s flagger Advance podinfo.test canary weight 40
Normal Synced 45s flagger Advance podinfo.test canary weight 45
Normal Synced 35s flagger Advance podinfo.test canary weight 50
Normal Synced 25s flagger Copying podinfo.test template spec to podinfo-primary.test
Normal Synced 5s flagger Promotion completed! Scaling down podinfo.test
```
A canary deployment is triggered everytime a new Knative Revision is created.
**Note** that if you apply new changes to the Knative Service during the canary analysis, Flagger will restart the analysis.
You can monitor how Flagger progressively changes the Knative Service object to spread traffic between Knative Revisions:
```bash
watch kubectl get httproute -n test podinfo -o=jsonpath='{.spec.traffic}'
```
You can monitor all canaries with:
```bash
watch kubectl get canaries --all-namespaces
NAMESPACE NAME STATUS WEIGHT LASTTRANSITIONTIME
test podinfo Progressing 15 2025-03-16T14:05:07Z
prod frontend Succeeded 0 2025-03-16T16:15:07Z
prod backend Failed 0 2025-03-16T17:05:07Z
```
## Automated rollback
During the canary analysis you can generate HTTP 500 errors and high latency to test if Flagger pauses the rollout.
Trigger another canary deployment:
```bash
kubectl -n test set image deployment/podinfo \
podinfod=stefanprodan/podinfo:6.0.2
```
Exec into the load tester pod with:
```bash
kubectl -n test exec -it flagger-loadtester-xx-xx sh
```
Generate HTTP 500 errors:
```bash
watch curl http://podinfo-canary:9898/status/500
```
Generate latency:
```bash
watch curl http://podinfo-canary:9898/delay/1
```
When the number of failed checks reaches the canary analysis threshold, the traffic is routed back to the primary
Knative Revision and the rollout is marked as failed.
```text
kubectl -n test describe canary/podinfo
Status:
Canary Weight: 0
Failed Checks: 10
Phase: Failed
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Synced 3m flagger Starting canary deployment for podinfo.test
Normal Synced 3m flagger Advance podinfo.test canary weight 5
Normal Synced 3m flagger Advance podinfo.test canary weight 10
Normal Synced 3m flagger Advance podinfo.test canary weight 15
Normal Synced 3m flagger Halt podinfo.test advancement error rate 69.17% > 1%
Normal Synced 2m flagger Halt podinfo.test advancement error rate 61.39% > 1%
Normal Synced 2m flagger Halt podinfo.test advancement error rate 55.06% > 1%
Normal Synced 2m flagger Halt podinfo.test advancement error rate 47.00% > 1%
Normal Synced 2m flagger (combined from similar events): Halt podinfo.test advancement error rate 38.08% > 1%
Warning Synced 1m flagger Rolling back podinfo.test failed checks threshold reached 10
Warning Synced 1m flagger Canary failed! Scaling down podinfo.test
```

View File

@ -3,7 +3,7 @@
Flagger can run automated application analysis, promotion and rollback for the following deployment strategies:
* **Canary Release** \(progressive traffic shifting\)
* Istio, Linkerd, App Mesh, NGINX, Skipper, Contour, Gloo Edge, Traefik, Open Service Mesh, Kuma, Gateway API, Apache APISIX
* Istio, Linkerd, App Mesh, NGINX, Skipper, Contour, Gloo Edge, Traefik, Open Service Mesh, Kuma, Gateway API, Apache APISIX, Knative
* **A/B Testing** \(HTTP headers and cookies traffic routing\)
* Istio, App Mesh, NGINX, Contour, Gloo Edge, Gateway API
* **Blue/Green** \(traffic switching\)
@ -407,7 +407,7 @@ cookie based routing with regular weight based routing. This means once a user i
version of our application (based on the traffic weights), they're always routed to that version, i.e.
they're never routed back to the old version of our application.
You can enable this, by specifying `.spec.analsyis.sessionAffinity` in the Canary:
You can enable this, by specifying `.spec.analysis.sessionAffinity` in the Canary:
```yaml
analysis:
@ -450,3 +450,47 @@ the Canary deployment:
```
Set-Cookie: flagger-cookie=McxKdLQoIN; Max-Age=21600
```
### Configuring stickiness for Primary deployment
The above strategy is helpful because it makes sure that any user that's routed to the Canary deployment
once is always routed to that deployment. But, this can results in an imbalance in the traffic shifting,
as over time, most of the traffic flows to the Canary deployment. To ensure fair traffic distribution, we
can also configure stickiness for the Primary deployment. You can configure this by specifying a
`primaryCookieName` field:
```yaml
analysis:
# schedule interval (default 60s)
interval: 1m
sessionAffinity:
# name of the cookie used
cookieName: flagger-cookie
# max age of the cookie (in seconds)
# optional; defaults to 86400
maxAge: 21600
# name of the cookie to use for the primary backend
# optional; unset means no primary stickiness
primaryCookieName: primary-flagger-cookie
```
> Note: This is only supported for the Gateway API provider for now.
Let's understand what the above configuration does. All the session affinity stuff in the above section
still occurs, but now the response header for requests routed to the primary deployment also include a
`Set-Cookie` header:
```
Set-Cookie: primary-flagger-cookie=ApvLdqCoMF; Max-Age=60
```
Note that the age of the cookie is the same as the Canary analysis's interval. This means that the cookie
expires when a new steps of the analysis begins and a new cookie is generated like so:
```
Set-Cookie: primary-flagger-cookie=BRtlVaQoPC; Max-Age=60
```
This ensures that, if the first request of a user during a particular step is routed to the primary deployment,
then all subsequent requests will be routed to the same until the next step starts. During a new step, a new cookie
value is generated which is then included in the headers of responses from the primary workload. This allows for
weighted traffic routing to happen while ensuring that users don't ever switch back to the primary deployment from
the canary deployment during a Canary analysis.

View File

@ -147,6 +147,7 @@ spec:
appProtocol: http
targetPort: 9898
portDiscovery: true
headless: false
```
The container port from the target workload should match the `service.port` or `service.targetPort`.
@ -155,6 +156,7 @@ The `service.targetPort` can be a container port number or name.
The `service.portName` is optional (defaults to `http`), if your workload uses gRPC then set the port name to `grpc`.
The `service.appProtocol` is optional, more details can be found [here](https://kubernetes.io/docs/concepts/services-networking/service/#application-protocol).
If port discovery is enabled, Flagger scans the target workload and extracts the containers ports
excluding the port specified in the canary service and service mesh sidecar ports.
These ports will be used when generating the ClusterIP services.
@ -204,6 +206,9 @@ Note that the `apex` annotations are added to both the generated Kubernetes Serv
generated service mesh/ingress object. This allows using external-dns with Istio `VirtualServices`
and `TraefikServices`. Beware of configuration conflicts [here](../faq.md#ExternalDNS).
If you want for the generated Kubernetes ClusterIP services to be [headless](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services),
then set `service.headless` to true.
Besides port mapping and metadata, the service specification can
contain URI match and rewrite rules, timeout and retry polices:

View File

@ -730,3 +730,54 @@ Only relevant if the `type` is set to `analysis`.
For the type `analysis`, the value returned by the provider is either `0`
(if the analysis failed), or `1` (analysis passed).
## Splunk
You can create custom metric checks using the Splunk provider.
Create a secret that contains your authentication token that can be found in the Splunk o11y UI.
```yaml
apiVersion: v1
kind: Secret
metadata:
name: splunk
namespace: istio-system
data:
sf_token_key: your-access-token
```
Splunk template example:
```yaml
apiVersion: flagger.app/v1beta1
kind: MetricTemplate
metadata:
name: success-rate
namespace: istio-system
spec:
provider:
type: splunk
address: https://api.<REALM>.signalfx.com
secretRef:
name: splunk
query: |
total = data('traces.count', filter=filter('sf_service', '{{target}}')).sum().publish(enable=False)
success = data('traces.count', filter=filter('sf_service', '{{target}}') and filter('sf_error', 'false')).sum().publish(enable=False)
((success/total) * 100).publish()
```
The query format documentation can be found [here](https://dev.splunk.com/observability/docs/signalflow).
Reference the template in the canary analysis:
```yaml
analysis:
metrics:
- name: "success rate"
templateRef:
name: success-rate
namespace: istio-system
thresholdRange:
max: 99
interval: 1m
```

112
go.mod
View File

@ -1,100 +1,108 @@
module github.com/fluxcd/flagger
go 1.23.0
go 1.24.0
require (
cloud.google.com/go/monitoring v1.21.2
cloud.google.com/go/monitoring v1.24.1
github.com/Masterminds/semver/v3 v3.3.1
github.com/aws/aws-sdk-go v1.55.5
github.com/aws/aws-sdk-go v1.55.6
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/go-logr/zapr v1.3.0
github.com/google/go-cmp v0.6.0
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/googleapis/gax-go/v2 v2.14.0
github.com/googleapis/gax-go/v2 v2.14.1
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/influxdata/influxdb-client-go/v2 v2.13.0
github.com/prometheus/client_golang v1.20.5
github.com/stretchr/testify v1.9.0
github.com/influxdata/influxdb-client-go/v2 v2.14.0
github.com/prometheus/client_golang v1.21.1
github.com/signalfx/signalflow-client-go v0.1.0
github.com/signalfx/signalfx-go v1.46.0
github.com/stretchr/testify v1.10.0
go.uber.org/zap v1.27.0
golang.org/x/sync v0.9.0
google.golang.org/api v0.205.0
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.35.1
golang.org/x/sync v0.12.0
google.golang.org/api v0.228.0
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463
google.golang.org/grpc v1.71.0
google.golang.org/protobuf v1.36.6
gopkg.in/h2non/gock.v1 v1.1.2
k8s.io/api v0.31.3
k8s.io/apimachinery v0.31.3
k8s.io/client-go v0.31.3
k8s.io/code-generator v0.31.3
k8s.io/api v0.32.3
k8s.io/apimachinery v0.32.3
k8s.io/client-go v0.32.3
k8s.io/code-generator v0.31.4
k8s.io/klog/v2 v2.130.1
knative.dev/serving v0.44.0
)
require (
cloud.google.com/go/auth v0.10.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect
cloud.google.com/go/compute/metadata v0.5.2 // indirect
cloud.google.com/go/auth v0.15.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blendle/zapdriver v1.3.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.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.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-containerregistry v0.13.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mailru/easyjson v0.7.7 // 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/oapi-codegen/runtime v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // 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/common v0.55.0 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect
go.opentelemetry.io/otel v1.34.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect
go.opentelemetry.io/otel/trace v1.34.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/term v0.26.0 // indirect
golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/oauth2 v0.28.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.29.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // 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
k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 // indirect
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
knative.dev/networking v0.0.0-20250117155906-67d1c274ba6a // indirect
knative.dev/pkg v0.0.0-20250117084104-c43477f0052b // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

336
go.sum
View File

@ -1,45 +1,51 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go/auth v0.10.1 h1:TnK46qldSfHWt2a0b/hciaiVJsmDXWy9FqyUan0uYiI=
cloud.google.com/go/auth v0.10.1/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI=
cloud.google.com/go/auth/oauth2adapt v0.2.5 h1:2p29+dePqsCHPP1bqDJcKj4qxRyYCcbzKpFyKGt3MTk=
cloud.google.com/go/auth/oauth2adapt v0.2.5/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/monitoring v1.21.2 h1:FChwVtClH19E7pJ+e0xUhJPGksctZNVOk2UhMmblmdU=
cloud.google.com/go/monitoring v1.21.2/go.mod h1:hS3pXvaG8KgWTSz+dAdyzPrGUYmi2Q+WFX8g2hqVEZU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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.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/monitoring v1.24.1 h1:vKiypZVFD/5a3BbQMvI4gZdl8445ITzXFh257XBgrS0=
cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0=
contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d h1:LblfooH1lKOpp1hIhukktmSAxFkqMPFk9KR6iZ0MJNI=
contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d/go.mod h1:IshRmMJBhDfFj5Y67nVhMYTTIze91RUeT73ipWKs/GY=
contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg=
contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ=
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/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE=
github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
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/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
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/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.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.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=
@ -47,58 +53,44 @@ 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-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
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.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
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/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.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.5.0/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.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.13.0 h1:y1C7Z3e149OJbOPDBxLYR8ITPz8dTKqQwjErKVHJC8k=
github.com/google/go-containerregistry v0.13.0/go.mod h1:J9FQ+eSS4a1aC2GNZxvNpbWhgp0487v+cgiilB4FqDo=
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-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM=
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/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.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o=
github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk=
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 h1:CWyXh/jylQWp2dtiV33mY4iSSp6yf4lmn+c7/tN+ObI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0/go.mod h1:nCLIt0w3Ept2NwF8ThLmrppXsfT07oC8k0XNDxd8sVU=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@ -107,14 +99,12 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/influxdata/influxdb-client-go/v2 v2.13.0 h1:ioBbLmR5NMbAjP4UVA5r9b5xGjpABD7j65pI8kFphDM=
github.com/influxdata/influxdb-client-go/v2 v2.13.0/go.mod h1:k+spCbt9hcvqvUiz0sr5D8LolXHqAAOfPw9v/RIRHl4=
github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4=
github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU=
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
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 v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY=
github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@ -124,13 +114,10 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
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.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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/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=
@ -152,153 +139,131 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
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/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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
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.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0=
github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI=
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/signalfx/signalflow-client-go v0.1.0 h1:aqyt+st3/y8x8JtuwYRL9pOkOTJb+KeCoRWi0SuY5vw=
github.com/signalfx/signalflow-client-go v0.1.0/go.mod h1:mY4DTAZuLHyMNGBjSrNdCg5kUU0hSkYjukAnjsVbsQs=
github.com/signalfx/signalfx-go v1.46.0 h1:bNPUmoOGjFCyVYz6arShOot4AnZe2knqw9N+UyZ+nMw=
github.com/signalfx/signalfx-go v1.46.0/go.mod h1:I30umyhRTu8mPpEtMzEbG0z9wOYjkUKTp9U0gFxFsmk=
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/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
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/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
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.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
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-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-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-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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/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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-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.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
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.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
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.205.0 h1:LFaxkAIpDb/GsrWV20dMMo5MR0h8UARTbn24LmD+0Pg=
google.golang.org/api v0.205.0/go.mod h1:NrK1EMqO8Xk6l6QwRAmrXXg2v6dzukhlOyvkYtnvUuc=
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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 h1:Q3nlH8iSQSRUwOskjbcSMcF2jiYMNiQYZ0c2KEJLKKU=
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38/go.mod h1:xBI+tzfqGGN2JBeSebfKXFSdBpWVQ7sLW40PTupVRm4=
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 h1:M0KvPgPmDZHPlbRbaNU1APr28TvwvvdUPlSv7PUvy8g=
google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:dguCy7UOdZhTvLzDyt15+rOrawrpM4q7DD9dQ1P11P4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
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.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
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.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs=
google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4=
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 h1:qEFnJI6AnfZk0NNe8YTyXQh5i//Zxi4gBHwRgp76qpw=
google.golang.org/genproto v0.0.0-20250324211829-b45e905df463/go.mod h1:SqIx1NV9hcvqdLHo7uNZDS5lrUJybQ3evo3+z/WBfA0=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/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.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=
@ -311,30 +276,33 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8=
k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE=
k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4=
k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4=
k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs=
k8s.io/code-generator v0.31.3 h1:Pj0fYOBms+ZrsulLi4DMsCEx1jG8fWKRLy44onHsLBI=
k8s.io/code-generator v0.31.3/go.mod h1:/umCIlT84g1+Yu5ZXtP1KGSRTnGiIzzX5AzUAxsNlts=
k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo=
k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8=
k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
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/code-generator v0.31.4 h1:Vu+8fKz+239rKiVDHFVHgjQ162cg5iUQPtTyQbwXeQw=
k8s.io/code-generator v0.31.4/go.mod h1:yMDt13Kn7m4MMZ4LxB1KBzdZjEyxzdT4b4qXq+lnI90=
k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 h1:si3PfKm8dDYxgfbeA6orqrtLkvvIeH8UqffFJDl0bz4=
k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
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-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
knative.dev/networking v0.0.0-20250117155906-67d1c274ba6a h1:FaDPXtv42+AkYh/mE269pttPSZ3fDVAjJiEsYUaM4SM=
knative.dev/networking v0.0.0-20250117155906-67d1c274ba6a/go.mod h1:AIKYMfZydhwXR/60c/3KXEnqEnH6aNEEqulifdqJVcQ=
knative.dev/pkg v0.0.0-20250117084104-c43477f0052b h1:a+gP7Yzu5NmoX2w1p8nfTgmSKF+aHLKGzqYT82ijJTw=
knative.dev/pkg v0.0.0-20250117084104-c43477f0052b/go.mod h1:bedSpkdLybR6JhL1J7XDLpd+JMKM/x8M5Apr80i5TeE=
knative.dev/serving v0.44.0 h1:c6TXhoSAI6eXt0/1ET3C69jMWYA4ES9FskSan/fBaac=
knative.dev/serving v0.44.0/go.mod h1:9bFONngDZtkdYZkP5ko9LDS9ZelnFY9SaPoHKG0vFxs=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@ -36,6 +36,10 @@ chmod +x ${CODEGEN_PKG}/kube_codegen.sh
source ${CODEGEN_PKG}/kube_codegen.sh
kube::codegen::gen_helpers \
--boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \
./pkg/apis
kube::codegen::gen_client \
--output-dir "${TEMP_DIR}/${PACKAGE_PATH_BASE}/pkg/client" \
--output-pkg "${PACKAGE_PATH_BASE}/pkg/client" \

View File

@ -80,7 +80,6 @@ spec:
type: object
required:
- targetRef
- service
- analysis
properties:
provider:
@ -198,6 +197,9 @@ spec:
portDiscovery:
description: Enable port dicovery
type: boolean
headless:
description: Headless if set to true, generates headless Kubernetes services.
type: boolean
timeout:
description: HTTP or gRPC request timeout
type: string
@ -1153,6 +1155,9 @@ spec:
cookieName:
description: CookieName is the key that will be used for the session affinity cookie.
type: string
primaryCookieName:
description: CookieName is the key that will be used for the session affinity cookie.
type: string
maxAge:
description: MaxAge indicates the number of seconds until the session affinity cookie will expire.
default: 86400
@ -1304,9 +1309,17 @@ spec:
- graphite
- dynatrace
- keptn
- splunk
address:
description: API address of this provider
type: string
headers:
description: Headers to add to HTTP(S) requests
type: object
additionalProperties:
type: array
items:
type: string
secretRef:
description: Kubernetes secret reference containing the provider credentials
type: object

View File

@ -9,4 +9,4 @@ resources:
images:
- name: ghcr.io/fluxcd/flagger
newName: ghcr.io/fluxcd/flagger
newTag: 1.39.0
newTag: 1.41.0

View File

@ -254,10 +254,24 @@ rules:
- update
- patch
- delete
- apiGroups:
- serving.knative.dev
resources:
- services
verbs:
- get
- update
- apiGroups:
- serving.knative.dev
resources:
- revisions
verbs:
- get
- nonResourceURLs:
- /version
verbs:
- get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@ -0,0 +1,8 @@
namespace: flagger-system
resources:
- namespace.yaml
bases:
- ../base/flagger/
- ../base/prometheus/
patchesStrategicMerge:
- patch.yaml

View File

@ -0,0 +1,9 @@
apiVersion: v1
kind: Namespace
metadata:
name: flagger-system
annotations:
linkerd.io/inject: disabled
labels:
istio-injection: disabled
appmesh.k8s.aws/sidecarInjectorWebhook: disabled

View File

@ -0,0 +1,14 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: flagger
spec:
template:
spec:
containers:
- name: flagger
args:
- -log-level=info
- -include-label-prefix=app.kubernetes.io
- -mesh-provider=knative
- -metrics-server=http://flagger-prometheus:9090

View File

@ -19,7 +19,7 @@ spec:
spec:
containers:
- name: loadtester
image: ghcr.io/fluxcd/flagger-loadtester:0.33.0
image: ghcr.io/fluxcd/flagger-loadtester:0.35.0
imagePullPolicy: IfNotPresent
ports:
- name: http

View File

@ -146,6 +146,11 @@ type CanaryService struct {
// PortDiscovery adds all container ports to the generated Kubernetes service
PortDiscovery bool `json:"portDiscovery"`
// Headless if set to true, generates headless Kubernetes services.
// ref: https://kubernetes.io/docs/concepts/services-networking/service/#headless-services
// +optional
Headless bool `json:"headless,omitempty"`
// Timeout of the HTTP or gRPC request
// +optional
Timeout string `json:"timeout,omitempty"`
@ -290,6 +295,9 @@ type SessionAffinity struct {
// The default value is 86,400 seconds, i.e. a day.
// +optional
MaxAge int `json:"maxAge,omitempty"`
// PrimaryCookieName is the key that will be used for the primary session affinity cookie.
// +optional
PrimaryCookieName string `json:"primaryCookieName,omitempty"`
}
// CanaryMetric holds the reference to metrics used for canary analysis
@ -458,6 +466,14 @@ type LocalObjectReference struct {
Name string `json:"name"`
}
func (l *LocalObjectReference) IsKnativeService() bool {
if l.Kind == "Service" && l.APIVersion == "serving.knative.dev/v1" {
return true
}
return false
}
type AutoscalerRefernce struct {
// API version of the scaler
// +required

View File

@ -17,6 +17,7 @@ limitations under the License.
package v1beta1
import (
"net/http"
"text/template"
corev1 "k8s.io/api/core/v1"
@ -67,6 +68,10 @@ type MetricTemplateProvider struct {
// +optional
Address string `json:"address,omitempty"`
// Headers to be supplied to HTTP(S) request of this provider
// +optional
Headers http.Header `json:"headers,omitempty"`
// Secret reference containing the provider credentials
// +optional
SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"`

View File

@ -7,6 +7,7 @@ const (
IstioProvider string = "istio"
SMIProvider string = "smi"
ContourProvider string = "contour"
KnativeProvider string = "knative"
GlooProvider string = "gloo"
NGINXProvider string = "nginx"
KubernetesProvider string = "kubernetes"

View File

@ -22,6 +22,8 @@ limitations under the License.
package v1beta1
import (
http "net/http"
gatewayapiv1beta1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1"
istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1"
v1 "k8s.io/api/core/v1"
@ -810,6 +812,21 @@ func (in *MetricTemplateModel) DeepCopy() *MetricTemplateModel {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MetricTemplateProvider) DeepCopyInto(out *MetricTemplateProvider) {
*out = *in
if in.Headers != nil {
in, out := &in.Headers, &out.Headers
*out = make(http.Header, len(*in))
for key, val := range *in {
var outVal []string
if val == nil {
(*out)[key] = nil
} else {
in, out := &val, &outVal
*out = make([]string, len(*in))
copy(*out, *in)
}
(*out)[key] = outVal
}
}
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(v1.LocalObjectReference)

View File

@ -1,4 +1,6 @@
// Package v1beta1 contains API Schema definitions for the
// gateway.networking.k8s.io API group.
// +k8s:deepcopy-gen=package
package v1beta1

View File

@ -18,6 +18,7 @@ import (
)
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:object:root=true
// +kubebuilder:resource:categories=gateway-api
// +kubebuilder:storageversion
@ -40,6 +41,7 @@ type HTTPRoute struct {
Status HTTPRouteStatus `json:"status,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:object:root=true
// HTTPRouteList contains a list of HTTPRoute.

View File

@ -2,11 +2,14 @@
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -14,13 +17,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by controller-gen. DO NOT EDIT.
// Code generated by deepcopy-gen. DO NOT EDIT.
package v1beta1
import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
@ -46,6 +49,7 @@ func (in *BackendObjectReference) DeepCopyInto(out *BackendObjectReference) {
*out = new(PortNumber)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendObjectReference.
@ -67,6 +71,7 @@ func (in *BackendRef) DeepCopyInto(out *BackendRef) {
*out = new(int32)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendRef.
@ -89,6 +94,7 @@ func (in *CommonRouteSpec) DeepCopyInto(out *CommonRouteSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonRouteSpec.
@ -112,6 +118,7 @@ func (in *HTTPBackendRef) DeepCopyInto(out *HTTPBackendRef) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPBackendRef.
@ -127,6 +134,7 @@ func (in *HTTPBackendRef) DeepCopy() *HTTPBackendRef {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HTTPHeader) DeepCopyInto(out *HTTPHeader) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPHeader.
@ -157,6 +165,7 @@ func (in *HTTPHeaderFilter) DeepCopyInto(out *HTTPHeaderFilter) {
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPHeaderFilter.
@ -177,6 +186,7 @@ func (in *HTTPHeaderMatch) DeepCopyInto(out *HTTPHeaderMatch) {
*out = new(HeaderMatchType)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPHeaderMatch.
@ -202,6 +212,7 @@ func (in *HTTPPathMatch) DeepCopyInto(out *HTTPPathMatch) {
*out = new(string)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPPathMatch.
@ -227,6 +238,7 @@ func (in *HTTPPathModifier) DeepCopyInto(out *HTTPPathModifier) {
*out = new(string)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPPathModifier.
@ -247,6 +259,7 @@ func (in *HTTPQueryParamMatch) DeepCopyInto(out *HTTPQueryParamMatch) {
*out = new(QueryParamMatchType)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPQueryParamMatch.
@ -263,6 +276,7 @@ func (in *HTTPQueryParamMatch) DeepCopy() *HTTPQueryParamMatch {
func (in *HTTPRequestMirrorFilter) DeepCopyInto(out *HTTPRequestMirrorFilter) {
*out = *in
in.BackendRef.DeepCopyInto(&out.BackendRef)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRequestMirrorFilter.
@ -303,6 +317,7 @@ func (in *HTTPRequestRedirectFilter) DeepCopyInto(out *HTTPRequestRedirectFilter
*out = new(int)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRequestRedirectFilter.
@ -322,6 +337,7 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) {
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRoute.
@ -375,6 +391,7 @@ func (in *HTTPRouteFilter) DeepCopyInto(out *HTTPRouteFilter) {
*out = new(LocalObjectReference)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteFilter.
@ -399,6 +416,7 @@ func (in *HTTPRouteList) DeepCopyInto(out *HTTPRouteList) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteList.
@ -446,6 +464,7 @@ func (in *HTTPRouteMatch) DeepCopyInto(out *HTTPRouteMatch) {
*out = new(HTTPMethod)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteMatch.
@ -482,6 +501,7 @@ func (in *HTTPRouteRule) DeepCopyInto(out *HTTPRouteRule) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteRule.
@ -510,6 +530,7 @@ func (in *HTTPRouteSpec) DeepCopyInto(out *HTTPRouteSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteSpec.
@ -526,6 +547,7 @@ func (in *HTTPRouteSpec) DeepCopy() *HTTPRouteSpec {
func (in *HTTPRouteStatus) DeepCopyInto(out *HTTPRouteStatus) {
*out = *in
in.RouteStatus.DeepCopyInto(&out.RouteStatus)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteStatus.
@ -551,6 +573,7 @@ func (in *HTTPURLRewriteFilter) DeepCopyInto(out *HTTPURLRewriteFilter) {
*out = new(HTTPPathModifier)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPURLRewriteFilter.
@ -566,6 +589,7 @@ func (in *HTTPURLRewriteFilter) DeepCopy() *HTTPURLRewriteFilter {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LocalObjectReference) DeepCopyInto(out *LocalObjectReference) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalObjectReference.
@ -606,6 +630,7 @@ func (in *ParentReference) DeepCopyInto(out *ParentReference) {
*out = new(PortNumber)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParentReference.
@ -629,6 +654,7 @@ func (in *RouteParentStatus) DeepCopyInto(out *RouteParentStatus) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteParentStatus.
@ -651,6 +677,7 @@ func (in *RouteStatus) DeepCopyInto(out *RouteStatus) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteStatus.
@ -681,6 +708,7 @@ func (in *SecretObjectReference) DeepCopyInto(out *SecretObjectReference) {
*out = new(Namespace)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretObjectReference.

View File

@ -1,6 +1,22 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package v1

View File

@ -103,6 +103,8 @@ type AdvancedConfig struct {
// HorizontalPodAutoscalerConfig specifies horizontal scale config
type HorizontalPodAutoscalerConfig struct {
// +optional
Name string `json:"name,omitempty"`
// +optional
Behavior *autoscalingv2beta2.HorizontalPodAutoscalerBehavior `json:"behavior,omitempty"`
}

View File

@ -20,12 +20,15 @@ import (
"go.uber.org/zap"
"k8s.io/client-go/kubernetes"
"github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned"
knative "knative.dev/serving/pkg/client/clientset/versioned"
)
type Factory struct {
kubeClient kubernetes.Interface
flaggerClient clientset.Interface
knativeClient knative.Interface
logger *zap.SugaredLogger
configTracker Tracker
labels []string
@ -34,6 +37,7 @@ type Factory struct {
func NewFactory(kubeClient kubernetes.Interface,
flaggerClient clientset.Interface,
knativeClient knative.Interface,
configTracker Tracker,
labels []string,
includeLabelPrefix []string,
@ -41,6 +45,7 @@ func NewFactory(kubeClient kubernetes.Interface,
return &Factory{
kubeClient: kubeClient,
flaggerClient: flaggerClient,
knativeClient: knativeClient,
logger: logger,
configTracker: configTracker,
labels: labels,
@ -48,7 +53,7 @@ func NewFactory(kubeClient kubernetes.Interface,
}
}
func (factory *Factory) Controller(kind string) Controller {
func (factory *Factory) Controller(obj v1beta1.LocalObjectReference) Controller {
deploymentCtrl := &DeploymentController{
logger: factory.logger,
kubeClient: factory.kubeClient,
@ -71,14 +76,22 @@ func (factory *Factory) Controller(kind string) Controller {
flaggerClient: factory.flaggerClient,
includeLabelPrefix: factory.includeLabelPrefix,
}
knativeCtrl := &KnativeController{
flaggerClient: factory.flaggerClient,
knativeClient: factory.knativeClient,
}
switch kind {
switch obj.Kind {
case "DaemonSet":
return daemonSetCtrl
case "Deployment":
return deploymentCtrl
case "Service":
if obj.IsKnativeService() {
return knativeCtrl
} else {
return serviceCtrl
}
default:
return deploymentCtrl
}

View File

@ -0,0 +1,161 @@
package canary
import (
"context"
"fmt"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned"
"go.uber.org/zap"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
serving "knative.dev/serving/pkg/apis/serving/v1"
knative "knative.dev/serving/pkg/client/clientset/versioned"
)
type KnativeController struct {
flaggerClient clientset.Interface
knativeClient knative.Interface
logger *zap.SugaredLogger
}
// IsPrimaryReady checks if the primary revision is ready
func (kc *KnativeController) IsPrimaryReady(cd *flaggerv1.Canary) (bool, error) {
service, err := kc.knativeClient.ServingV1().Services(cd.Namespace).Get(context.TODO(), cd.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
return true, fmt.Errorf("Knative Service %s.%s get query error: %w", cd.Spec.TargetRef.Name, cd.Namespace, err)
}
revisionName, exists := service.Annotations["flagger.app/primary-revision"]
if !exists {
return true, fmt.Errorf("Knative Service %s.%s primary revision annotation not found", cd.Spec.TargetRef.Name, cd.Namespace)
}
revision, err := kc.knativeClient.ServingV1().Revisions(cd.Namespace).Get(context.TODO(), revisionName, metav1.GetOptions{})
if err != nil {
return true, fmt.Errorf("Knative Revision %s.%s get query error: %w", revisionName, cd.Namespace, err)
}
if !revision.IsReady() {
return true, fmt.Errorf("Knative Revision %s.%s is not ready", revision.Name, cd.Namespace)
}
return true, nil
}
// IsCanaryReady checks if the canary revision is ready
func (kc *KnativeController) IsCanaryReady(cd *flaggerv1.Canary) (bool, error) {
service, err := kc.knativeClient.ServingV1().Services(cd.Namespace).Get(context.TODO(), cd.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
return true, fmt.Errorf("Knative Service %s.%s get query error: %w", cd.Spec.TargetRef.Name, cd.Namespace, err)
}
revision, err := kc.knativeClient.ServingV1().Revisions(cd.Namespace).Get(context.TODO(), service.Status.LatestCreatedRevisionName, metav1.GetOptions{})
if err != nil {
return true, fmt.Errorf("Knative Revision %s.%s get query error: %w", service.Status.LatestCreatedRevisionName, cd.Namespace, err)
}
if !revision.IsReady() {
return true, fmt.Errorf("Knative Revision %s.%s is not ready", revision.Name, cd.Namespace)
}
return true, nil
}
func (kc *KnativeController) GetMetadata(canary *flaggerv1.Canary) (string, string, map[string]int32, error) {
return "", "", make(map[string]int32), nil
}
// SyncStatus encodes list of revisions and updates the canary status
func (kc *KnativeController) SyncStatus(cd *flaggerv1.Canary, status flaggerv1.CanaryStatus) error {
service, err := kc.knativeClient.ServingV1().Services(cd.Namespace).Get(context.TODO(), cd.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("Knative Service %s.%s get query error: %w", cd.Spec.TargetRef.Name, cd.Namespace, err)
}
return syncCanaryStatus(kc.flaggerClient, cd, status, service.Status.LatestCreatedRevisionName, func(copy *flaggerv1.Canary) {})
}
// SetStatusFailedChecks updates the canary failed checks counter
func (kc *KnativeController) SetStatusFailedChecks(cd *flaggerv1.Canary, val int) error {
return setStatusFailedChecks(kc.flaggerClient, cd, val)
}
// SetStatusWeight updates the canary status weight value
func (kc *KnativeController) SetStatusWeight(cd *flaggerv1.Canary, val int) error {
return setStatusWeight(kc.flaggerClient, cd, val)
}
// SetStatusIterations updates the canary status iterations value
func (kc *KnativeController) SetStatusIterations(cd *flaggerv1.Canary, val int) error {
return setStatusIterations(kc.flaggerClient, cd, val)
}
// SetStatusPhase updates the canary status phase
func (kc *KnativeController) SetStatusPhase(cd *flaggerv1.Canary, phase flaggerv1.CanaryPhase) error {
return setStatusPhase(kc.flaggerClient, cd, phase)
}
// Initialize configures the Knative Service to be used for canary rollouts.
func (kc *KnativeController) Initialize(cd *flaggerv1.Canary) (bool, error) {
if cd.Status.Phase == "" || cd.Status.Phase == flaggerv1.CanaryPhaseInitializing {
service, err := kc.knativeClient.ServingV1().Services(cd.Namespace).Get(context.TODO(), cd.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
return true, fmt.Errorf("Knative Service %s.%s get query error: %w", cd.Spec.TargetRef.Name, cd.Namespace, err)
}
if service.Annotations == nil {
service.Annotations = make(map[string]string, 0)
}
service.Annotations["flagger.app/primary-revision"] = service.Status.LatestCreatedRevisionName
canaryPercent := int64(0)
primaryPercent := int64(100)
latestRevision := true
traffic := []serving.TrafficTarget{
{
LatestRevision: &latestRevision,
Percent: &canaryPercent,
},
{
RevisionName: service.Status.LatestCreatedRevisionName,
Percent: &primaryPercent,
},
}
service.Spec.Traffic = traffic
_, err = kc.knativeClient.ServingV1().Services(cd.Namespace).Update(context.TODO(), service, metav1.UpdateOptions{})
if err != nil {
return true, fmt.Errorf("Knative Service %s.%s update query error %w", cd.Spec.TargetRef.Name, cd.Namespace, err)
}
}
return true, nil
}
func (kc *KnativeController) Promote(cd *flaggerv1.Canary) error {
service, err := kc.knativeClient.ServingV1().Services(cd.Namespace).Get(context.TODO(), cd.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("Knative Service %s.%s get query error: %w", cd.Spec.TargetRef.Name, cd.Namespace, err)
}
service.Annotations["flagger.app/primary-revision"] = service.Status.LatestCreatedRevisionName
_, err = kc.knativeClient.ServingV1().Services(cd.Namespace).Update(context.TODO(), service, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("Knative Service %s.%s update query error %w", cd.Spec.TargetRef.Name, cd.Namespace, err)
}
return nil
}
func (kc *KnativeController) HasTargetChanged(cd *flaggerv1.Canary) (bool, error) {
service, err := kc.knativeClient.ServingV1().Services(cd.Namespace).Get(context.TODO(), cd.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
return true, fmt.Errorf("Knative Service %s.%s get query error: %w", cd.Spec.TargetRef.Name, cd.Namespace, err)
}
return hasSpecChanged(cd, service.Status.LatestCreatedRevisionName)
}
func (kc *KnativeController) HaveDependenciesChanged(canary *flaggerv1.Canary) (bool, error) {
return false, nil
}
func (kc *KnativeController) ScaleToZero(canary *flaggerv1.Canary) error {
return nil
}
func (kc *KnativeController) ScaleFromZero(canary *flaggerv1.Canary) error {
return nil
}
func (kc *KnativeController) Finalize(canary *flaggerv1.Canary) error {
return nil
}

View File

@ -0,0 +1,79 @@
package canary
import (
"context"
"testing"
"github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestKnativeController_Promote(t *testing.T) {
mocks := newKnativeServiceFixture("podinfo")
_, err := mocks.controller.Initialize(mocks.canary)
require.NoError(t, err)
service, err := mocks.knativeClient.ServingV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
service.Status.LatestCreatedRevisionName = "latest-revision"
_, err = mocks.knativeClient.ServingV1().Services("default").UpdateStatus(context.TODO(), service, metav1.UpdateOptions{})
require.NoError(t, err)
err = mocks.controller.Promote(mocks.canary)
require.NoError(t, err)
service, err = mocks.knativeClient.ServingV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, "latest-revision", service.Annotations["flagger.app/primary-revision"])
}
func TestKnativeController_Initialize(t *testing.T) {
mocks := newKnativeServiceFixture("podinfo")
mocks.canary.Status.Phase = v1beta1.CanaryPhasePromoting
ok, err := mocks.controller.Initialize(mocks.canary)
require.NoError(t, err)
assert.Equal(t, true, ok)
service, err := mocks.knativeClient.ServingV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
assert.Len(t, service.Annotations, 0)
assert.Len(t, service.Spec.Traffic, 0)
mocks.canary.Status.Phase = v1beta1.CanaryPhaseInitializing
ok, err = mocks.controller.Initialize(mocks.canary)
require.NoError(t, err)
assert.Equal(t, true, ok)
service, err = mocks.knativeClient.ServingV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, "podinfo-00001", service.Annotations["flagger.app/primary-revision"])
assert.Len(t, service.Spec.Traffic, 2)
assert.Equal(t, *service.Spec.Traffic[0].Percent, int64(0))
assert.True(t, *service.Spec.Traffic[0].LatestRevision)
assert.Equal(t, *service.Spec.Traffic[1].Percent, int64(100))
assert.Equal(t, service.Spec.Traffic[1].RevisionName, "podinfo-00001")
}
func TestKnativeController_HasTargetChanged(t *testing.T) {
mocks := newKnativeServiceFixture("podinfo")
_, err := mocks.controller.Initialize(mocks.canary)
require.NoError(t, err)
service, err := mocks.knativeClient.ServingV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
mocks.canary.Status.LastAppliedSpec = ComputeHash(service.Status.LatestCreatedRevisionName)
service.Status.LatestCreatedRevisionName = "latest-revision"
_, err = mocks.knativeClient.ServingV1().Services("default").UpdateStatus(context.TODO(), service, metav1.UpdateOptions{})
require.NoError(t, err)
ok, err := mocks.controller.HasTargetChanged(mocks.canary)
require.NoError(t, err)
assert.True(t, ok)
}

View File

@ -0,0 +1,99 @@
package canary
import (
"go.uber.org/zap"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned"
fakeFlagger "github.com/fluxcd/flagger/pkg/client/clientset/versioned/fake"
"github.com/fluxcd/flagger/pkg/logger"
serving "knative.dev/serving/pkg/apis/serving/v1"
knative "knative.dev/serving/pkg/client/clientset/versioned"
fakeKnative "knative.dev/serving/pkg/client/clientset/versioned/fake"
)
type knativeControllerFixture struct {
canary *flaggerv1.Canary
flaggerClient clientset.Interface
knativeClient knative.Interface
controller KnativeController
logger *zap.SugaredLogger
}
func newKnativeServiceFixture(name string) knativeControllerFixture {
canary := newKnativeControllerTestCanary(name)
flaggerClient := fakeFlagger.NewSimpleClientset(canary)
knativeClient := fakeKnative.NewSimpleClientset(newKnativeControllerTestService(name))
logger, _ := logger.NewLogger("debug")
ctrl := KnativeController{
flaggerClient: flaggerClient,
knativeClient: knativeClient,
logger: logger,
}
return knativeControllerFixture{
canary: canary,
controller: ctrl,
logger: logger,
flaggerClient: flaggerClient,
knativeClient: knativeClient,
}
}
func newKnativeControllerTestService(name string) *serving.Service {
s := &serving.Service{
TypeMeta: metav1.TypeMeta{APIVersion: serving.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
},
Spec: serving.ServiceSpec{
ConfigurationSpec: serving.ConfigurationSpec{
Template: serving.RevisionTemplateSpec{
Spec: serving.RevisionSpec{
PodSpec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "podinfo",
Image: "quay.io/stefanprodan/podinfo:1.2.0",
},
},
},
},
},
},
},
Status: serving.ServiceStatus{
ConfigurationStatusFields: serving.ConfigurationStatusFields{
LatestCreatedRevisionName: "podinfo-00001",
},
},
}
return s
}
func newKnativeControllerTestCanary(name string) *flaggerv1.Canary {
cd := &flaggerv1.Canary{
TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "podinfo",
},
Spec: flaggerv1.CanarySpec{
Provider: "knative",
TargetRef: flaggerv1.LocalObjectReference{
Name: name,
APIVersion: "serving.knative.dev/v1",
Kind: "Service",
},
Analysis: &flaggerv1.CanaryAnalysis{},
},
}
return cd
}

View File

@ -17,6 +17,7 @@ import (
"github.com/google/go-cmp/cmp"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// ScaledObjectReconciler is a ScalerReconciler that reconciles KEDA ScaledObjects.
@ -47,6 +48,8 @@ func (sor *ScaledObjectReconciler) reconcilePrimaryScaler(cd *flaggerv1.Canary,
setPrimaryScaledObjectQueries(cd, targetSoClone.Spec.Triggers)
setPrimaryScaledObjectHPA(targetSoClone)
soSpec := keda.ScaledObjectSpec{
ScaleTargetRef: &keda.ScaleTarget{
Name: primaryName,
@ -77,7 +80,8 @@ func (sor *ScaledObjectReconciler) reconcilePrimaryScaler(cd *flaggerv1.Canary,
primarySo, err := sor.flaggerClient.KedaV1alpha1().ScaledObjects(cd.Namespace).Get(context.TODO(), primarySoName, metav1.GetOptions{})
if errors.IsNotFound(err) {
primarySo = &keda.ScaledObject{
ObjectMeta: makeObjectMeta(primarySoName, targetSoClone.Labels, cd),
// Passing in the annotations from the targetSo so that they are carried over to the primarySo. This is required so that the transfer ownership annotation can be added.
ObjectMeta: makeObjectMetaSo(primarySoName, targetSoClone.Labels, targetSoClone.Annotations, cd),
Spec: soSpec,
}
_, err = sor.flaggerClient.KedaV1alpha1().ScaledObjects(cd.Namespace).Create(context.TODO(), primarySo, metav1.CreateOptions{})
@ -206,3 +210,33 @@ func setPrimaryScaledObjectQueries(cd *flaggerv1.Canary, triggers []keda.ScaleTr
}
}
}
func makeObjectMetaSo(name string, labels map[string]string, annotations map[string]string, cd *flaggerv1.Canary) metav1.ObjectMeta {
return metav1.ObjectMeta{
Name: name,
Namespace: cd.Namespace,
Labels: filterMetadata(labels),
Annotations: filterMetadata(annotations),
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(cd, schema.GroupVersionKind{
Group: flaggerv1.SchemeGroupVersion.Group,
Version: flaggerv1.SchemeGroupVersion.Version,
Kind: flaggerv1.CanaryKind,
}),
},
}
}
func setPrimaryScaledObjectHPA(targetSoClone *keda.ScaledObject) {
if targetSoClone.Spec.Advanced == nil {
targetSoClone.Spec.Advanced = &keda.AdvancedConfig{}
}
if targetSoClone.Spec.Advanced.HorizontalPodAutoscalerConfig == nil {
targetSoClone.Spec.Advanced.HorizontalPodAutoscalerConfig = &keda.HorizontalPodAutoscalerConfig{}
}
if targetSoClone.Spec.Advanced.HorizontalPodAutoscalerConfig.Name != "" {
// if the target scaled object has the hpa name set, then append "-primary" to the primary scaled object hpa name
// if the target scaled object does not have the hpa name set, then it will use the default set by keda
targetSoClone.Spec.Advanced.HorizontalPodAutoscalerConfig.Name = fmt.Sprintf("%s-primary", targetSoClone.Spec.Advanced.HorizontalPodAutoscalerConfig.Name)
}
}

View File

@ -29,6 +29,10 @@ func Test_reconcilePrimaryScaledObject(t *testing.T) {
primarySO, err := mocks.flaggerClient.KedaV1alpha1().ScaledObjects("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{})
require.NoError(t, err)
// test that the hpa ownership annotation is added to the primarySO
assert.Equal(t, primarySO.ObjectMeta.Annotations["scaledobject.keda.sh/transfer-hpa-ownership"], "true")
// test that the horizontalpodautoscalerconfig is set to 'podinfo-primary', so that it takes over ownership of the HPA
assert.Equal(t, primarySO.Spec.Advanced.HorizontalPodAutoscalerConfig.Name, "podinfo-primary")
assert.Equal(t, primarySO.Spec.ScaleTargetRef.Name, fmt.Sprintf("%s-primary", mocks.canary.Spec.TargetRef.Name))
assert.Equal(t, int(*primarySO.Spec.PollingInterval), 10)
assert.Equal(t, int(*primarySO.Spec.MinReplicaCount), 1)

View File

@ -154,11 +154,19 @@ func newScaledObject() *keda.ScaledObject {
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "podinfo",
Annotations: map[string]string{
"scaledobject.keda.sh/transfer-hpa-ownership": "true",
},
},
Spec: keda.ScaledObjectSpec{
ScaleTargetRef: &keda.ScaleTarget{
Name: "podinfo",
},
Advanced: &keda.AdvancedConfig{
HorizontalPodAutoscalerConfig: &keda.HorizontalPodAutoscalerConfig{
Name: "podinfo",
},
},
PollingInterval: int32p(10),
MinReplicaCount: int32p(1),
MaxReplicaCount: int32p(4),

View File

@ -45,6 +45,7 @@ import (
"github.com/fluxcd/flagger/pkg/metrics/observers"
"github.com/fluxcd/flagger/pkg/notifier"
"github.com/fluxcd/flagger/pkg/router"
knative "knative.dev/serving/pkg/client/clientset/versioned"
)
const controllerAgentName = "flagger"
@ -53,6 +54,7 @@ const controllerAgentName = "flagger"
type Controller struct {
kubeConfig *rest.Config
kubeClient kubernetes.Interface
knativeClient knative.Interface
flaggerClient clientset.Interface
flaggerInformers Informers
flaggerSynced cache.InformerSynced
@ -81,6 +83,7 @@ type Informers struct {
func NewController(
kubeClient kubernetes.Interface,
knativeClient knative.Interface,
flaggerClient clientset.Interface,
flaggerInformers Informers,
flaggerWindow time.Duration,
@ -111,6 +114,7 @@ func NewController(
ctrl := &Controller{
kubeConfig: kubeConfig,
kubeClient: kubeClient,
knativeClient: knativeClient,
flaggerClient: flaggerClient,
flaggerInformers: flaggerInformers,
flaggerSynced: flaggerInformers.CanaryInformer.Informer().HasSynced,
@ -330,6 +334,13 @@ func (c *Controller) verifyCanary(canary *flaggerv1.Canary) error {
return err
}
}
if err := verifyKnativeCanary(canary); err != nil {
return err
}
if err := verifySessionAffinity(canary); err != nil {
return err
}
return nil
}
@ -352,6 +363,34 @@ func verifyNoCrossNamespaceRefs(canary *flaggerv1.Canary) error {
return nil
}
func verifyKnativeCanary(canary *flaggerv1.Canary) error {
if canary.Spec.TargetRef.IsKnativeService() != (canary.Spec.Provider == flaggerv1.KnativeProvider) {
if canary.Spec.TargetRef.IsKnativeService() {
return fmt.Errorf("can't use %s provider with Knative Service as target", canary.Spec.Provider)
}
return fmt.Errorf("can't use %s/%s as target if provider is set to knative",
canary.Spec.TargetRef.APIVersion, canary.Spec.TargetRef.Kind)
}
if canary.Spec.TargetRef.IsKnativeService() {
if canary.Spec.AutoscalerRef != nil {
return fmt.Errorf("can't use autoscaler with Knative Service")
}
}
return nil
}
func verifySessionAffinity(canary *flaggerv1.Canary) error {
if canary.Spec.Analysis.SessionAffinity != nil {
if canary.Spec.Analysis.SessionAffinity.CookieName == canary.Spec.Analysis.SessionAffinity.PrimaryCookieName {
return fmt.Errorf("can't use the same cookie name for both primary and cookie name; please update them to be different")
}
}
return nil
}
func checkCustomResourceType(obj interface{}, logger *zap.SugaredLogger) (flaggerv1.Canary, bool) {
var roll *flaggerv1.Canary
var ok bool

View File

@ -26,6 +26,7 @@ func TestController_verifyCanary(t *testing.T) {
Name: "upstream",
Namespace: "test",
},
Analysis: &flaggerv1.CanaryAnalysis{},
},
},
wantErr: true,
@ -42,6 +43,7 @@ func TestController_verifyCanary(t *testing.T) {
Name: "upstream",
Namespace: "default",
},
Analysis: &flaggerv1.CanaryAnalysis{},
},
},
wantErr: false,
@ -90,6 +92,100 @@ func TestController_verifyCanary(t *testing.T) {
},
wantErr: true,
},
{
name: "knative provider with non-knative service should return an error",
canary: flaggerv1.Canary{
ObjectMeta: metav1.ObjectMeta{
Name: "cd-1",
Namespace: "default",
},
Spec: flaggerv1.CanarySpec{
Provider: "knative",
TargetRef: flaggerv1.LocalObjectReference{
Kind: "Deployment",
Name: "podinfo",
},
Analysis: &flaggerv1.CanaryAnalysis{},
},
},
wantErr: true,
},
{
name: "knative service with non-knative provider should return an error",
canary: flaggerv1.Canary{
ObjectMeta: metav1.ObjectMeta{
Name: "cd-1",
Namespace: "default",
},
Spec: flaggerv1.CanarySpec{
Provider: "istio",
TargetRef: flaggerv1.LocalObjectReference{
Kind: "Service",
APIVersion: "serving.knative.dev/v1",
Name: "podinfo",
},
Analysis: &flaggerv1.CanaryAnalysis{},
},
},
wantErr: true,
},
{
name: "knative service with autoscaler ref should return an error",
canary: flaggerv1.Canary{
ObjectMeta: metav1.ObjectMeta{
Name: "cd-1",
Namespace: "default",
},
Spec: flaggerv1.CanarySpec{
Provider: "knative",
AutoscalerRef: &flaggerv1.AutoscalerRefernce{},
TargetRef: flaggerv1.LocalObjectReference{
Kind: "Service",
APIVersion: "serving.knative.dev/v1",
Name: "podinfo",
},
Analysis: &flaggerv1.CanaryAnalysis{},
},
},
wantErr: true,
},
{
name: "knative service with knative provider is okay",
canary: flaggerv1.Canary{
ObjectMeta: metav1.ObjectMeta{
Name: "cd-1",
Namespace: "default",
},
Spec: flaggerv1.CanarySpec{
Provider: "knative",
TargetRef: flaggerv1.LocalObjectReference{
Kind: "Service",
APIVersion: "serving.knative.dev/v1",
Name: "podinfo",
},
Analysis: &flaggerv1.CanaryAnalysis{},
},
},
wantErr: false,
},
{
name: "session affinity with same cookie names should return an error",
canary: flaggerv1.Canary{
ObjectMeta: metav1.ObjectMeta{
Name: "cd-1",
Namespace: "default",
},
Spec: flaggerv1.CanarySpec{
Analysis: &flaggerv1.CanaryAnalysis{
SessionAffinity: &flaggerv1.SessionAffinity{
CookieName: "smth",
PrimaryCookieName: "smth",
},
},
},
},
wantErr: true,
},
}
ctrl := &Controller{

View File

@ -43,7 +43,7 @@ func (c *Controller) finalize(old interface{}) error {
}
// Retrieve a controller
canaryController := c.canaryFactory.Controller(canary.Spec.TargetRef.Kind)
canaryController := c.canaryFactory.Controller(canary.Spec.TargetRef)
// Set the status to terminating if not already in that state
if canary.Status.Phase != flaggerv1.CanaryPhaseTerminating {

View File

@ -168,7 +168,7 @@ func (c *Controller) advanceCanary(name string, namespace string) {
msg := "skipping canary run as object is suspended"
c.logger.With("canary", fmt.Sprintf("%s.%s", name, namespace)).
Debug(msg)
c.recordEventInfof(cd, msg)
c.recordEventInfof(cd, "%s", msg)
return
}
@ -179,7 +179,7 @@ func (c *Controller) advanceCanary(name string, namespace string) {
}
// init controller based on target kind
canaryController := c.canaryFactory.Controller(cd.Spec.TargetRef.Kind)
canaryController := c.canaryFactory.Controller(cd.Spec.TargetRef)
labelSelector, labelValue, ports, err := canaryController.GetMetadata(cd)
if err != nil {

View File

@ -92,7 +92,7 @@ func newDaemonSetFixture(c *flaggerv1.Canary) daemonSetFixture {
}
// init router
rf := router.NewFactory(nil, kubeClient, flaggerClient, "annotationsPrefix", "", logger, flaggerClient, true)
rf := router.NewFactory(nil, kubeClient, flaggerClient, nil, "annotationsPrefix", "", logger, flaggerClient, true)
// init observer
observerFactory, _ := observers.NewFactory(testMetricsServerURL)
@ -103,7 +103,7 @@ func newDaemonSetFixture(c *flaggerv1.Canary) daemonSetFixture {
KubeClient: kubeClient,
FlaggerClient: flaggerClient,
}
canaryFactory := canary.NewFactory(kubeClient, flaggerClient, configTracker, []string{"app", "name"}, []string{""}, logger)
canaryFactory := canary.NewFactory(kubeClient, flaggerClient, nil, configTracker, []string{"app", "name"}, []string{""}, logger)
ctrl := &Controller{
kubeClient: kubeClient,
@ -130,7 +130,9 @@ func newDaemonSetFixture(c *flaggerv1.Canary) daemonSetFixture {
return daemonSetFixture{
canary: c,
deployer: canaryFactory.Controller("DaemonSet"),
deployer: canaryFactory.Controller(flaggerv1.LocalObjectReference{
Kind: "DaemonSet",
}),
logger: logger,
flaggerClient: flaggerClient,
meshClient: flaggerClient,

View File

@ -121,7 +121,7 @@ func newDeploymentFixture(c *flaggerv1.Canary) fixture {
}
// init router
rf := router.NewFactory(nil, kubeClient, flaggerClient, "annotationsPrefix", "", logger, flaggerClient, true)
rf := router.NewFactory(nil, kubeClient, flaggerClient, nil, "annotationsPrefix", "", logger, flaggerClient, true)
// init observer
observerFactory, _ := observers.NewFactory(testMetricsServerURL)
@ -132,7 +132,7 @@ func newDeploymentFixture(c *flaggerv1.Canary) fixture {
KubeClient: kubeClient,
FlaggerClient: flaggerClient,
}
canaryFactory := canary.NewFactory(kubeClient, flaggerClient, configTracker, []string{"app", "name"}, []string{""}, logger)
canaryFactory := canary.NewFactory(kubeClient, flaggerClient, nil, configTracker, []string{"app", "name"}, []string{""}, logger)
ctrl := &Controller{
kubeClient: kubeClient,
@ -160,7 +160,9 @@ func newDeploymentFixture(c *flaggerv1.Canary) fixture {
return fixture{
canary: c,
deployer: canaryFactory.Controller("Deployment"),
deployer: canaryFactory.Controller(flaggerv1.LocalObjectReference{
Kind: "Deployment",
}),
logger: logger,
flaggerClient: flaggerClient,
meshClient: flaggerClient,

View File

@ -28,6 +28,7 @@ import (
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
"github.com/fluxcd/flagger/pkg/metrics/observers"
"github.com/fluxcd/flagger/pkg/metrics/providers"
serving "knative.dev/serving/pkg/apis/serving/v1"
)
const (
@ -110,10 +111,20 @@ func (c *Controller) runBuiltinMetricChecks(canary *flaggerv1.Canary) bool {
}
}
// set the metrics provider to query Prometheus for the canary Kubernetes service if the canary target is Service
if canary.Spec.TargetRef.Kind == "Service" {
if canary.Spec.TargetRef.Kind == "Service" && !canary.Spec.TargetRef.IsKnativeService() {
metricsProvider = metricsProvider + MetricsProviderServiceSuffix
}
var knativeService *serving.Service
if canary.Spec.Provider == flaggerv1.KnativeProvider || c.meshProvider == flaggerv1.KnativeProvider {
var err error
knativeService, err = c.knativeClient.ServingV1().Services(canary.Namespace).Get(context.TODO(), canary.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
c.recordEventErrorf(canary, "Error fetching Knative service %s/%s %v", canary.Namespace, canary.Spec.TargetRef.Name, err)
return false
}
}
// create observer based on the mesh provider
observerFactory := c.observerFactory
@ -135,7 +146,11 @@ func (c *Controller) runBuiltinMetricChecks(canary *flaggerv1.Canary) bool {
}
if metric.Name == "request-success-rate" {
val, err := observer.GetRequestSuccessRate(toMetricModel(canary, metric.Interval, metric.TemplateVariables))
model := toMetricModel(canary, metric.Interval, metric.TemplateVariables)
if knativeService != nil {
model.Route = knativeService.Status.LatestCreatedRevisionName
}
val, err := observer.GetRequestSuccessRate(model)
if err != nil {
if errors.Is(err, providers.ErrNoValuesFound) {
c.recordEventWarningf(canary,
@ -167,7 +182,11 @@ func (c *Controller) runBuiltinMetricChecks(canary *flaggerv1.Canary) bool {
}
if metric.Name == "request-duration" {
val, err := observer.GetRequestDuration(toMetricModel(canary, metric.Interval, metric.TemplateVariables))
model := toMetricModel(canary, metric.Interval, metric.TemplateVariables)
if knativeService != nil {
model.Route = knativeService.Status.LatestCreatedRevisionName
}
val, err := observer.GetRequestDuration(model)
if err != nil {
if errors.Is(err, providers.ErrNoValuesFound) {
c.recordEventWarningf(canary, "Halt advancement no values found for %s metric %s probably %s.%s is not receiving traffic",
@ -199,7 +218,11 @@ func (c *Controller) runBuiltinMetricChecks(canary *flaggerv1.Canary) bool {
// in-line PromQL
if metric.Query != "" {
query, err := observers.RenderQuery(metric.Query, toMetricModel(canary, metric.Interval, metric.TemplateVariables))
model := toMetricModel(canary, metric.Interval, metric.TemplateVariables)
if knativeService != nil {
model.Route = knativeService.Status.LatestCreatedRevisionName
}
query, err := observers.RenderQuery(metric.Query, model)
val, err := observerFactory.Client.RunQuery(query)
if err != nil {
if errors.Is(err, providers.ErrNoValuesFound) {
@ -235,6 +258,16 @@ func (c *Controller) runBuiltinMetricChecks(canary *flaggerv1.Canary) bool {
}
func (c *Controller) runMetricChecks(canary *flaggerv1.Canary) bool {
var knativeService *serving.Service
if canary.Spec.Provider == flaggerv1.KnativeProvider || c.meshProvider == flaggerv1.KnativeProvider {
var err error
knativeService, err = c.knativeClient.ServingV1().Services(canary.Namespace).Get(context.TODO(), canary.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
c.recordEventErrorf(canary, "Error fetching Knative service %s/%s %v", canary.Namespace, canary.Spec.TargetRef.Name, err)
return false
}
}
for _, metric := range canary.GetAnalysis().Metrics {
if metric.TemplateRef != nil {
namespace := canary.Namespace
@ -267,7 +300,11 @@ func (c *Controller) runMetricChecks(canary *flaggerv1.Canary) bool {
return false
}
query, err := observers.RenderQuery(template.Spec.Query, toMetricModel(canary, metric.Interval, metric.TemplateVariables))
model := toMetricModel(canary, metric.Interval, metric.TemplateVariables)
if knativeService != nil {
model.Route = knativeService.Status.LatestCreatedRevisionName
}
query, err := observers.RenderQuery(template.Spec.Query, model)
c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, namespace)).
Debugf("Metric template %s.%s query: %s", metric.TemplateRef.Name, namespace, query)
if err != nil {

View File

@ -92,6 +92,10 @@ func (factory Factory) Observer(provider string) Interface {
return &ApisixObserver{
client: factory.Client,
}
case provider == flaggerv1.KnativeProvider:
return &KnativeObserver{
client: factory.Client,
}
default:
return &IstioObserver{
client: factory.Client,

View File

@ -0,0 +1,92 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package observers
import (
"fmt"
"time"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
"github.com/fluxcd/flagger/pkg/metrics/providers"
)
//envoy_cluster_name="default/hello-00001"
var knativeQueries = map[string]string{
"request-success-rate": `
sum(
rate(
envoy_cluster_upstream_rq{
envoy_cluster_name=~"{{ namespace }}/{{ route }}",
envoy_response_code!~"5.*"
}[{{ interval }}]
)
)
/
sum(
rate(
envoy_cluster_upstream_rq{
envoy_cluster_name=~"{{ namespace }}/{{ route }}",
}[{{ interval }}]
)
)
* 100`,
"request-duration": `
histogram_quantile(
0.99,
sum(
rate(
envoy_cluster_upstream_rq_time_bucket{
envoy_cluster_name=~"{{ namespace }}/{{ route }}",
}[{{ interval }}]
)
) by (le)
)`,
}
type KnativeObserver struct {
client providers.Interface
}
func (ob *KnativeObserver) GetRequestSuccessRate(model flaggerv1.MetricTemplateModel) (float64, error) {
query, err := RenderQuery(knativeQueries["request-success-rate"], model)
if err != nil {
return 0, fmt.Errorf("rendering query failed: %w", err)
}
value, err := ob.client.RunQuery(query)
if err != nil {
return 0, fmt.Errorf("running query failed: %w", err)
}
return value, nil
}
func (ob *KnativeObserver) GetRequestDuration(model flaggerv1.MetricTemplateModel) (time.Duration, error) {
query, err := RenderQuery(knativeQueries["request-duration"], model)
if err != nil {
return 0, fmt.Errorf("rendering query failed: %w", err)
}
value, err := ob.client.RunQuery(query)
if err != nil {
return 0, fmt.Errorf("running query failed: %w", err)
}
ms := time.Duration(int64(value)) * time.Millisecond
return ms, nil
}

View File

@ -0,0 +1,102 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package observers
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
"github.com/fluxcd/flagger/pkg/metrics/providers"
)
func TestKnativeObserver_GetRequestSuccessRate(t *testing.T) {
expected := ` sum( rate( envoy_cluster_upstream_rq{ envoy_cluster_name=~"default/podinfo-00001", envoy_response_code!~"5.*" }[1m] ) ) / sum( rate( envoy_cluster_upstream_rq{ envoy_cluster_name=~"default/podinfo-00001", }[1m] ) ) * 100`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
promql := r.URL.Query()["query"][0]
assert.Equal(t, expected, promql)
json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}`
w.Write([]byte(json))
}))
defer ts.Close()
client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
Type: "prometheus",
Address: ts.URL,
SecretRef: nil,
}, nil)
require.NoError(t, err)
observer := &KnativeObserver{
client: client,
}
val, err := observer.GetRequestSuccessRate(flaggerv1.MetricTemplateModel{
Name: "podinfo",
Namespace: "default",
Target: "podinfo",
Service: "podinfo",
Route: "podinfo-00001",
Interval: "1m",
})
require.NoError(t, err)
assert.Equal(t, float64(100), val)
}
func TestKnativeObserver_GetRequestDuration(t *testing.T) {
expected := ` histogram_quantile( 0.99, sum( rate( envoy_cluster_upstream_rq_time_bucket{ envoy_cluster_name=~"default/podinfo-00001", }[1m] ) ) by (le) )`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
promql := r.URL.Query()["query"][0]
assert.Equal(t, expected, promql)
json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}`
w.Write([]byte(json))
}))
defer ts.Close()
client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
Type: "prometheus",
Address: ts.URL,
SecretRef: nil,
}, nil)
require.NoError(t, err)
observer := &KnativeObserver{
client: client,
}
val, err := observer.GetRequestDuration(flaggerv1.MetricTemplateModel{
Name: "podinfo",
Namespace: "default",
Target: "podinfo",
Service: "podinfo",
Route: "podinfo-00001",
Interval: "1m",
})
require.NoError(t, err)
assert.Equal(t, 100*time.Millisecond, val)
}

View File

@ -143,16 +143,20 @@ func (p *DatadogProvider) RunQuery(query string) (float64, error) {
return 0, fmt.Errorf("invalid response: %s: %w", string(b), ErrNoValuesFound)
}
// in case of more than one series in the response, pick the first time series from the response
pl := res.Series[0].Pointlist
if len(pl) < 1 {
return 0, fmt.Errorf("invalid response: %s: %w", string(b), ErrNoValuesFound)
}
vs := pl[len(pl)-1]
// pick the first (oldest) timestamp/value pair from the time series, at the beginning of the interval
// must not pick the newest one from the end of the interval, since it almost always contains an incomplete bucket
vs := pl[0]
if len(vs) < 1 {
return 0, fmt.Errorf("invalid response: %s: %w", string(b), ErrNoValuesFound)
}
// return the second element of the pair: the value
return vs[1], nil
}

View File

@ -75,7 +75,7 @@ func TestDatadogProvider_RunQuery(t *testing.T) {
assert.GreaterOrEqual(t, to, now)
}
json := fmt.Sprintf(`{"series": [{"pointlist": [[1577232000000,29325.102158814265],[1577318400000,56294.46758591842],[1577404800000,%f]]}]}`, expected)
json := fmt.Sprintf(`{"series": [{"pointlist": [[1577232000000,%f],[1577318400000,56294.46758591842],[1577404800000,29325.102158814265]]}]}`, expected)
w.Write([]byte(json))
}))
defer ts.Close()

View File

@ -43,6 +43,8 @@ func (factory Factory) Provider(metricInterval string, provider flaggerv1.Metric
return NewDynatraceProvider(metricInterval, provider, credentials)
case "keptn":
return NewKeptnProvider(config)
case "splunk":
return NewSplunkProvider(metricInterval, provider, credentials)
default:
return NewPrometheusProvider(provider, credentials)
}

View File

@ -300,7 +300,7 @@ func TestGraphiteProvider_IsOnline(t *testing.T) {
}
w.WriteHeader(test.code)
fmt.Fprintf(w, test.body)
fmt.Fprintf(w, "%s", test.body)
}))
defer ts.Close()

View File

@ -39,6 +39,7 @@ const prometheusOnlineQuery = "vector(1)"
type PrometheusProvider struct {
timeout time.Duration
url url.URL
headers http.Header
username string
password string
token string
@ -69,6 +70,7 @@ func NewPrometheusProvider(provider flaggerv1.MetricTemplateProvider, credential
prom := PrometheusProvider{
timeout: 5 * time.Second,
url: *promURL,
headers: provider.Headers,
client: http.DefaultClient,
}
@ -116,6 +118,10 @@ func (p *PrometheusProvider) RunQuery(query string) (float64, error) {
return 0, fmt.Errorf("http.NewRequest failed: %w", err)
}
if p.headers != nil {
req.Header = p.headers
}
if p.token != "" {
req.Header.Add("Authorization", "Bearer "+p.token)
} else if p.username != "" && p.password != "" {

View File

@ -307,3 +307,40 @@ func TestPrometheusProvider_IsOnline(t *testing.T) {
assert.Equal(t, true, ok)
})
}
func TestPrometheusProvider_RunQueryWithProviderHeaders(t *testing.T) {
t.Run("ok", func(t *testing.T) {
expected := `sum(envoy_cluster_upstream_rq)`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
promql := r.URL.Query()["query"][0]
assert.Equal(t, expected, promql)
assert.Equal(t, []string{"tenant1"}, r.Header.Values("X-Scope-Orgid"))
json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1545905245.458,"100"]}]}}`
w.Write([]byte(json))
}))
defer ts.Close()
clients := prometheusFake()
template, err := clients.flaggerClient.FlaggerV1beta1().MetricTemplates("default").Get(context.TODO(), "prometheus", metav1.GetOptions{})
require.NoError(t, err)
template.Spec.Provider.Address = ts.URL
template.Spec.Provider.Headers = http.Header{
"X-Scope-OrgID": []string{"tenant1"},
}
secret, err := clients.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "prometheus", metav1.GetOptions{})
require.NoError(t, err)
prom, err := NewPrometheusProvider(template.Spec.Provider, secret.Data)
require.NoError(t, err)
val, err := prom.RunQuery(template.Spec.Query)
require.NoError(t, err)
assert.Equal(t, float64(100), val)
})
}

View File

@ -0,0 +1,195 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package providers
import (
"cmp"
"context"
"fmt"
"io"
"net/http"
"slices"
"strings"
"time"
"github.com/signalfx/signalflow-client-go/signalflow"
"github.com/signalfx/signalflow-client-go/signalflow/messages"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
)
// https://dev.splunk.com/observability/reference
const (
signalFxSignalFlowApiPath = "/v2/signalflow"
signalFxValidationPath = "/v2/metric?limit=1"
signalFxTokenSecretKey = "sf_token_key"
signalFxTokenHeaderKey = "X-SF-Token"
signalFxFromDeltaMultiplierOnMetricInterval = 10
)
// SplunkProvider executes signalfx queries
type SplunkProvider struct {
metricsQueryEndpoint string
apiValidationEndpoint string
timeout time.Duration
token string
fromDelta int64
}
type splunkResponse struct {
}
// NewSplunkProvider takes a canary spec, a provider spec and the credentials map, and
// returns a Splunk client ready to execute queries against the API
func NewSplunkProvider(metricInterval string,
provider flaggerv1.MetricTemplateProvider,
credentials map[string][]byte) (*SplunkProvider, error) {
address := provider.Address
if address == "" {
return nil, fmt.Errorf("splunk endpoint is not set")
}
sp := SplunkProvider{
timeout: 5 * time.Second,
// Convert the configured address to match the protocol of the respective API
// ex.
// https://api.<REALM>.signalfx.com -> wss://stream.<REALM>.signalfx.com
// wss://stream.<REALM>.signalfx.com -> wss://stream.<REALM>.signalfx.com
metricsQueryEndpoint: strings.Replace(strings.Replace(address+signalFxSignalFlowApiPath, "http", "ws", 1), "api", "stream", 1),
// ex.
// https://api.<REALM>.signalfx.com -> https://api.<REALM>.signalfx.com
// wss://stream.<REALM>.signalfx.com -> https://api.<REALM>.signalfx.com
apiValidationEndpoint: strings.Replace(strings.Replace(address+signalFxValidationPath, "ws", "http", 1), "stream", "api", 1),
}
if b, ok := credentials[signalFxTokenSecretKey]; ok {
sp.token = string(b)
} else {
return nil, fmt.Errorf("splunk credentials does not contain sf_token_key")
}
md, err := time.ParseDuration(metricInterval)
if err != nil {
return nil, fmt.Errorf("error parsing metric interval: %w", err)
}
sp.fromDelta = int64(signalFxFromDeltaMultiplierOnMetricInterval * md.Milliseconds())
return &sp, nil
}
// RunQuery executes the query and converts the first result to float64
func (p *SplunkProvider) RunQuery(query string) (float64, error) {
c, err := signalflow.NewClient(signalflow.StreamURL(p.metricsQueryEndpoint), signalflow.AccessToken(p.token))
if err != nil {
return 0, fmt.Errorf("error creating signalflow client: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
defer cancel()
now := time.Now().UnixMilli()
comp, err := c.Execute(ctx, &signalflow.ExecuteRequest{
Program: query,
Start: time.UnixMilli(now - p.fromDelta),
Stop: time.UnixMilli(now),
Immediate: true,
})
if err != nil {
return 0, fmt.Errorf("error executing query: %w", err)
}
payloads := p.receivePaylods(comp)
if comp.Err() != nil {
return 0, fmt.Errorf("error executing query: %w", comp.Err())
}
payloads = slices.DeleteFunc(payloads, func(msg messages.DataPayload) bool {
return msg.Value() == nil
})
if len(payloads) < 1 {
return 0, fmt.Errorf("invalid response: %w", ErrNoValuesFound)
}
// Error when a SignalFlow query returns two or more results.
// Since a different TSID is set for each metrics to be retrieved, eliminate duplicate TSIDs and determine if two or more TSIDs exist.
_payloads := slices.Clone(payloads)
slices.SortFunc(_payloads, func(i, j messages.DataPayload) int {
return cmp.Compare(i.TSID, j.TSID)
})
if len(slices.CompactFunc(_payloads, func(i, j messages.DataPayload) bool { return i.TSID == j.TSID })) > 1 {
return 0, fmt.Errorf("invalid response: %w", ErrMultipleValuesReturned)
}
payload := payloads[len(payloads)-1]
switch payload.Type {
case messages.ValTypeLong:
return float64(payload.Int64()), nil
case messages.ValTypeDouble:
return payload.Float64(), nil
case messages.ValTypeInt:
return float64(payload.Int32()), nil
default:
return 0, fmt.Errorf("invalid response: UnsupportedValueType")
}
}
func (p *SplunkProvider) receivePaylods(comp *signalflow.Computation) []messages.DataPayload {
payloads := []messages.DataPayload{}
for dataMsg := range comp.Data() {
if dataMsg == nil {
continue
}
payloads = append(payloads, dataMsg.Payloads...)
}
return payloads
}
// IsOnline calls the provider endpoint and returns an error if the API is unreachable
func (p *SplunkProvider) IsOnline() (bool, error) {
req, err := http.NewRequest("GET", p.apiValidationEndpoint, nil)
if err != nil {
return false, fmt.Errorf("error http.NewRequest: %w", err)
}
req.Header.Add(signalFxTokenHeaderKey, p.token)
ctx, cancel := context.WithTimeout(req.Context(), p.timeout)
defer cancel()
r, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
return false, fmt.Errorf("request failed: %w", err)
}
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
if err != nil {
return false, fmt.Errorf("error reading body: %w", err)
}
if r.StatusCode != http.StatusOK {
return false, fmt.Errorf("error response: %s", string(b))
}
return true, nil
}

View File

@ -0,0 +1,188 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package providers
import (
"errors"
"fmt"
"math/rand"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/signalfx/signalflow-client-go/signalflow"
"github.com/signalfx/signalflow-client-go/signalflow/messages"
"github.com/signalfx/signalfx-go/idtool"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
)
func TestNewSplunkProvider(t *testing.T) {
token := "token"
cs := map[string][]byte{
signalFxTokenSecretKey: []byte(token),
}
mi := "100s"
md, err := time.ParseDuration(mi)
require.NoError(t, err)
sp, err := NewSplunkProvider("100s", flaggerv1.MetricTemplateProvider{Address: "https://api.us1.signalfx.com"}, cs)
require.NoError(t, err)
assert.Equal(t, "wss://stream.us1.signalfx.com/v2/signalflow", sp.metricsQueryEndpoint)
assert.Equal(t, "https://api.us1.signalfx.com/v2/metric?limit=1", sp.apiValidationEndpoint)
assert.Equal(t, int64(md.Milliseconds()*signalFxFromDeltaMultiplierOnMetricInterval), sp.fromDelta)
assert.Equal(t, token, sp.token)
}
func TestSplunkProvider_RunQuery(t *testing.T) {
t.Run("ok", func(t *testing.T) {
fakeBackend := signalflow.NewRunningFakeBackend()
go func() {
<-time.After(3 * time.Second)
fakeBackend.Stop()
}()
tsids := []idtool.ID{idtool.ID(rand.Int63())}
var expected float64 = float64(len(tsids))
for i, tsid := range tsids {
fakeBackend.AddTSIDMetadata(tsid, &messages.MetadataProperties{
Metric: "service.request.count",
})
fakeBackend.SetTSIDFloatData(tsid, float64(i+1))
}
pg := `data('service.request.count', filter=filter('service.name', 'myservice')).sum().publish()`
fakeBackend.AddProgramTSIDs(pg, tsids)
parsedUrl, err := url.Parse(fakeBackend.URL())
require.NoError(t, err)
sp, err := NewSplunkProvider("1m",
flaggerv1.MetricTemplateProvider{Address: fmt.Sprintf("http://%s", parsedUrl.Host)},
map[string][]byte{
signalFxTokenSecretKey: []byte(fakeBackend.AccessToken),
},
)
require.NoError(t, err)
f, err := sp.RunQuery(pg)
require.NoError(t, err)
assert.Equal(t, expected, f)
})
t.Run("no values", func(t *testing.T) {
fakeBackend := signalflow.NewRunningFakeBackend()
go func() {
<-time.After(3 * time.Second)
fakeBackend.Stop()
}()
tsids := []idtool.ID{idtool.ID(rand.Int63()), idtool.ID(rand.Int63()), idtool.ID(rand.Int63())}
for _, tsid := range tsids {
fakeBackend.AddTSIDMetadata(tsid, &messages.MetadataProperties{
Metric: "service.request.count",
})
}
pg := `data('service.request.count', filter=filter('service.name', 'myservice')).sum().publish()`
fakeBackend.AddProgramTSIDs(pg, tsids)
parsedUrl, err := url.Parse(fakeBackend.URL())
require.NoError(t, err)
sp, err := NewSplunkProvider("1m",
flaggerv1.MetricTemplateProvider{Address: fmt.Sprintf("http://%s", parsedUrl.Host)},
map[string][]byte{
signalFxTokenSecretKey: []byte(fakeBackend.AccessToken),
},
)
require.NoError(t, err)
_, err = sp.RunQuery(pg)
require.True(t, errors.Is(err, ErrNoValuesFound))
})
t.Run("multiple values", func(t *testing.T) {
fakeBackend := signalflow.NewRunningFakeBackend()
go func() {
<-time.After(3 * time.Second)
fakeBackend.Stop()
}()
tsids := []idtool.ID{idtool.ID(rand.Int63()), idtool.ID(rand.Int63()), idtool.ID(rand.Int63())}
for i, tsid := range tsids {
fakeBackend.AddTSIDMetadata(tsid, &messages.MetadataProperties{
Metric: "service.request.count",
})
fakeBackend.SetTSIDFloatData(tsid, float64(i+1))
}
pg := `data('service.request.count', filter=filter('service.name', 'myservice')).sum().publish(); data('service.request.count', filter=filter('service.name', 'myservice2')).sum().publish()`
fakeBackend.AddProgramTSIDs(pg, tsids)
parsedUrl, err := url.Parse(fakeBackend.URL())
require.NoError(t, err)
sp, err := NewSplunkProvider("1m",
flaggerv1.MetricTemplateProvider{Address: fmt.Sprintf("http://%s", parsedUrl.Host)},
map[string][]byte{
signalFxTokenSecretKey: []byte(fakeBackend.AccessToken),
},
)
require.NoError(t, err)
_, err = sp.RunQuery(pg)
require.True(t, errors.Is(err, ErrMultipleValuesReturned))
})
}
func TestSplunkProvider_IsOnline(t *testing.T) {
for _, c := range []struct {
code int
errExpected bool
}{
{code: http.StatusOK, errExpected: false},
{code: http.StatusUnauthorized, errExpected: true},
} {
t.Run(fmt.Sprintf("%d", c.code), func(t *testing.T) {
token := "token"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, token, r.Header.Get(signalFxTokenHeaderKey))
w.WriteHeader(c.code)
}))
defer ts.Close()
sp, err := NewSplunkProvider("1m",
flaggerv1.MetricTemplateProvider{Address: ts.URL},
map[string][]byte{
signalFxTokenSecretKey: []byte(token),
},
)
require.NoError(t, err)
_, err = sp.IsOnline()
if c.errExpected {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@ -25,6 +25,7 @@ import (
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned"
knative "knative.dev/serving/pkg/client/clientset/versioned"
)
type Factory struct {
@ -32,6 +33,7 @@ type Factory struct {
kubeClient kubernetes.Interface
meshClient clientset.Interface
flaggerClient clientset.Interface
knativeClient knative.Interface
ingressAnnotationsPrefix string
ingressClass string
logger *zap.SugaredLogger
@ -40,6 +42,7 @@ type Factory struct {
func NewFactory(kubeConfig *restclient.Config, kubeClient kubernetes.Interface,
flaggerClient clientset.Interface,
knativeClient knative.Interface,
ingressAnnotationsPrefix string,
ingressClass string,
logger *zap.SugaredLogger,
@ -50,6 +53,7 @@ func NewFactory(kubeConfig *restclient.Config, kubeClient kubernetes.Interface,
meshClient: meshClient,
kubeClient: kubeClient,
flaggerClient: flaggerClient,
knativeClient: knativeClient,
ingressAnnotationsPrefix: ingressAnnotationsPrefix,
ingressClass: ingressClass,
logger: logger,
@ -150,6 +154,10 @@ func (factory *Factory) MeshRouter(provider string, labelSelector string) Interf
ingressClass: factory.ingressClass,
setOwnerRefs: factory.setOwnerRefs,
}
case provider == flaggerv1.KnativeProvider:
return &KnativeRouter{
knativeClient: factory.knativeClient,
}
case strings.HasPrefix(provider, flaggerv1.GlooProvider):
return &GlooRouter{
logger: factory.logger,

View File

@ -22,12 +22,8 @@ import (
"reflect"
"slices"
"strings"
"time"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
v1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1"
"github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1"
istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1"
clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"go.uber.org/zap"
@ -35,6 +31,12 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
v1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1"
"github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1"
istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1"
clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned"
)
var (
@ -189,20 +191,26 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error {
cmpopts.IgnoreFields(v1.BackendRef{}, "Weight"),
cmpopts.EquateEmpty(),
}
if canary.Spec.Analysis.SessionAffinity != nil {
ignoreRoute := cmpopts.IgnoreSliceElements(func(r v1.HTTPRouteRule) bool {
ignoreCookieRouteFunc := func(name string) func(r v1.HTTPRouteRule) bool {
return func(r v1.HTTPRouteRule) bool {
// Ignore the rule that does sticky routing, i.e. matches against the `Cookie` header.
for _, match := range r.Matches {
for _, headerMatch := range match.Headers {
if *headerMatch.Type == headerMatchRegex && headerMatch.Name == cookieHeader &&
strings.Contains(headerMatch.Value, canary.Spec.Analysis.SessionAffinity.CookieName) {
strings.Contains(headerMatch.Value, name) {
return true
}
}
}
return false
})
ignoreCmpOptions = append(ignoreCmpOptions, ignoreRoute)
}
}
ignoreCanaryRoute := cmpopts.IgnoreSliceElements(ignoreCookieRouteFunc(canary.Spec.Analysis.SessionAffinity.CookieName))
ignorePrimaryRoute := cmpopts.IgnoreSliceElements(ignoreCookieRouteFunc(canary.Spec.Analysis.SessionAffinity.PrimaryCookieName))
ignoreCmpOptions = append(ignoreCmpOptions, ignoreCanaryRoute, ignorePrimaryRoute)
// Ignore backend specific filters, since we use that to insert the `Set-Cookie` header in responses.
ignoreCmpOptions = append(ignoreCmpOptions, cmpopts.IgnoreFields(v1.HTTPBackendRef{}, "Filters"))
}
@ -218,16 +226,26 @@ func (gwr *GatewayAPIRouter) Reconcile(canary *flaggerv1.Canary) error {
}
if httpRoute != nil {
// Preserve the existing annotations added by other controllers such as AWS Gateway API Controller.
mergedAnnotations := newMetadata.Annotations
for key, val := range httpRoute.Annotations {
if _, ok := mergedAnnotations[key]; !ok {
mergedAnnotations[key] = val
}
}
// Compare the existing HTTPRoute spec and metadata with the desired state.
// If there are differences, update the HTTPRoute object.
specDiff := cmp.Diff(
httpRoute.Spec, httpRouteSpec,
ignoreCmpOptions...,
)
labelsDiff := cmp.Diff(newMetadata.Labels, httpRoute.Labels, cmpopts.EquateEmpty())
annotationsDiff := cmp.Diff(newMetadata.Annotations, httpRoute.Annotations, cmpopts.EquateEmpty())
annotationsDiff := cmp.Diff(mergedAnnotations, httpRoute.Annotations, cmpopts.EquateEmpty())
if (specDiff != "" && httpRoute.Name != "") || labelsDiff != "" || annotationsDiff != "" {
hrClone := httpRoute.DeepCopy()
hrClone.Spec = httpRouteSpec
hrClone.ObjectMeta.Annotations = newMetadata.Annotations
hrClone.ObjectMeta.Annotations = mergedAnnotations
hrClone.ObjectMeta.Labels = newMetadata.Labels
_, err := gwr.gatewayAPIClient.GatewayapiV1().HTTPRoutes(hrNamespace).
Update(context.TODO(), hrClone, metav1.UpdateOptions{})
@ -428,25 +446,38 @@ func (gwr *GatewayAPIRouter) Finalize(_ *flaggerv1.Canary) error {
return nil
}
func getBackendByServiceName(rule *v1.HTTPRouteRule, svcName string) *v1.HTTPBackendRef {
for i, backendRef := range rule.BackendRefs {
if string(backendRef.BackendObjectReference.Name) == svcName {
return &rule.BackendRefs[i]
}
}
return nil
}
// getSessionAffinityRouteRules returns the HTTPRouteRule objects required to perform
// session affinity based Canary releases.
func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Canary, canaryWeight int,
weightedRouteRule *v1.HTTPRouteRule) ([]v1.HTTPRouteRule, error) {
_, primarySvcName, canarySvcName := canary.GetServiceNames()
stickyRouteRule := *weightedRouteRule
stickyCanaryRouteRule := *weightedRouteRule
stickyPrimaryRouteRule := *weightedRouteRule
// If a canary run is active, we want all responses corresponding to requests hitting the canary deployment
// (due to weighted routing) to include a `Set-Cookie` header. All requests that have the `Cookie` header
// and match the value of the `Set-Cookie` header will be routed to the canary deployment.
if canaryWeight != 0 {
// if the status doesn't have the canary cookie, then generate a new canary cookie.
if canary.Status.SessionAffinityCookie == "" {
canary.Status.SessionAffinityCookie = fmt.Sprintf("%s=%s", canary.Spec.Analysis.SessionAffinity.CookieName, randSeq())
}
primaryCookie := fmt.Sprintf("%s=%s", canary.Spec.Analysis.SessionAffinity.PrimaryCookieName, randSeq())
// Add `Set-Cookie` header modifier to the primary backend in the weighted routing rule.
for i, backendRef := range weightedRouteRule.BackendRefs {
if string(backendRef.BackendObjectReference.Name) == canarySvcName {
backendRef.Filters = append(backendRef.Filters, v1.HTTPRouteFilter{
// add response modifier to the canary backend ref in the rule that does weighted routing
// to include the canary cookie.
canaryBackendRef := getBackendByServiceName(weightedRouteRule, canarySvcName)
canaryBackendRef.Filters = append(canaryBackendRef.Filters, v1.HTTPRouteFilter{
Type: v1.HTTPRouteFilterResponseHeaderModifier,
ResponseHeaderModifier: &v1.HTTPHeaderFilter{
Add: []v1.HTTPHeader{
@ -459,11 +490,32 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana
},
},
})
// add response modifier to the primary backend ref in the rule that does weighted routing
// to include the primary cookie, only if a primary cookie name has been specified.
if canary.Spec.Analysis.SessionAffinity.PrimaryCookieName != "" {
primaryBackendRef := getBackendByServiceName(weightedRouteRule, primarySvcName)
interval, err := time.ParseDuration(canary.Spec.Analysis.Interval)
if err != nil {
return nil, fmt.Errorf("failed to parse canary interval: %w", err)
}
weightedRouteRule.BackendRefs[i] = backendRef
primaryBackendRef.Filters = append(primaryBackendRef.Filters, v1.HTTPRouteFilter{
Type: v1.HTTPRouteFilterResponseHeaderModifier,
ResponseHeaderModifier: &v1.HTTPHeaderFilter{
Add: []v1.HTTPHeader{
{
Name: setCookieHeader,
Value: fmt.Sprintf("%s; %s=%d", primaryCookie, maxAgeAttr,
int(interval.Seconds()),
),
},
},
},
})
}
// Add `Cookie` header matcher to the sticky routing rule.
// configure the sticky canary rule to match against requests that match against the
// canary cookie and send them to the canary backend.
cookieKeyAndVal := strings.Split(canary.Status.SessionAffinityCookie, "=")
regexMatchType := v1.HeaderMatchRegularExpression
cookieMatch := v1.HTTPRouteMatch{
@ -482,8 +534,8 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana
}
mergedMatches := gwr.mergeMatchConditions([]v1.HTTPRouteMatch{cookieMatch}, svcMatches)
stickyRouteRule.Matches = mergedMatches
stickyRouteRule.BackendRefs = []v1.HTTPBackendRef{
stickyCanaryRouteRule.Matches = mergedMatches
stickyCanaryRouteRule.BackendRefs = []v1.HTTPBackendRef{
{
BackendRef: gwr.makeBackendRef(primarySvcName, 0, canary.Spec.Service.Port),
},
@ -491,6 +543,42 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana
BackendRef: gwr.makeBackendRef(canarySvcName, 100, canary.Spec.Service.Port),
},
}
// add a sticky primary rule to match against requests that match against the
// primary cookie and send them to the primary backend, only if a primary cookie name has
// been specified.
if canary.Spec.Analysis.SessionAffinity.PrimaryCookieName != "" {
cookieKeyAndVal = strings.Split(primaryCookie, "=")
regexMatchType = v1.HeaderMatchRegularExpression
primaryCookieMatch := v1.HTTPRouteMatch{
Headers: []v1.HTTPHeaderMatch{
{
Type: &regexMatchType,
Name: cookieHeader,
Value: fmt.Sprintf(".*%s.*%s.*", cookieKeyAndVal[0], cookieKeyAndVal[1]),
},
},
}
svcMatches, err = gwr.mapRouteMatches(canary.Spec.Service.Match)
if err != nil {
return nil, err
}
mergedMatches = gwr.mergeMatchConditions([]v1.HTTPRouteMatch{primaryCookieMatch}, svcMatches)
stickyPrimaryRouteRule.Matches = mergedMatches
stickyPrimaryRouteRule.BackendRefs = []v1.HTTPBackendRef{
{
BackendRef: gwr.makeBackendRef(primarySvcName, 100, canary.Spec.Service.Port),
},
{
BackendRef: gwr.makeBackendRef(canarySvcName, 0, canary.Spec.Service.Port),
},
}
return []v1.HTTPRouteRule{stickyCanaryRouteRule, stickyPrimaryRouteRule, *weightedRouteRule}, nil
}
return []v1.HTTPRouteRule{stickyCanaryRouteRule, *weightedRouteRule}, nil
} else {
// If canary weight is 0 and SessionAffinityCookie is non-blank, then it belongs to a previous canary run.
if canary.Status.SessionAffinityCookie != "" {
@ -513,9 +601,9 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana
}
svcMatches, _ := gwr.mapRouteMatches(canary.Spec.Service.Match)
mergedMatches := gwr.mergeMatchConditions([]v1.HTTPRouteMatch{cookieMatch}, svcMatches)
stickyRouteRule.Matches = mergedMatches
stickyCanaryRouteRule.Matches = mergedMatches
stickyRouteRule.Filters = append(stickyRouteRule.Filters, v1.HTTPRouteFilter{
stickyCanaryRouteRule.Filters = append(stickyCanaryRouteRule.Filters, v1.HTTPRouteFilter{
Type: v1.HTTPRouteFilterResponseHeaderModifier,
ResponseHeaderModifier: &v1.HTTPHeaderFilter{
Add: []v1.HTTPHeader{
@ -529,9 +617,8 @@ func (gwr *GatewayAPIRouter) getSessionAffinityRouteRules(canary *flaggerv1.Cana
}
canary.Status.SessionAffinityCookie = ""
return []v1.HTTPRouteRule{stickyCanaryRouteRule, *weightedRouteRule}, nil
}
return []v1.HTTPRouteRule{stickyRouteRule, *weightedRouteRule}, nil
}
func (gwr *GatewayAPIRouter) mapRouteMatches(requestMatches []istiov1beta1.HTTPMatchRequest) ([]v1.HTTPRouteMatch, error) {

View File

@ -21,6 +21,7 @@ import (
"fmt"
"strings"
"testing"
"time"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
v1 "github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1"
@ -57,6 +58,17 @@ func TestGatewayAPIRouter_Reconcile(t *testing.T) {
timeout := routeRules[0].Timeouts
assert.Equal(t, string(*timeout.Request), canary.Spec.Service.Timeout)
// assert that http route annotations injected by the networking controller is preserved.
httpRoute.Annotations["foo"] = "bar"
_, err = router.gatewayAPIClient.GatewayapiV1().HTTPRoutes("default").Update(context.TODO(), httpRoute, metav1.UpdateOptions{})
require.NoError(t, err)
err = router.Reconcile(canary)
require.NoError(t, err)
httpRoute, err = router.gatewayAPIClient.GatewayapiV1().HTTPRoutes("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, httpRoute.Annotations["foo"], "bar")
}
func TestGatewayAPIRouter_Routes(t *testing.T) {
@ -93,6 +105,7 @@ func TestGatewayAPIRouter_Routes(t *testing.T) {
_, pSvcName, cSvcName := canary.GetServiceNames()
err := router.SetRoutes(canary, 90, 10, false)
require.NoError(t, err)
hr, err := mocks.meshClient.GatewayapiV1().HTTPRoutes("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
@ -279,6 +292,7 @@ func TestGatewayAPIRouter_Routes(t *testing.T) {
}
func TestGatewayAPIRouter_getSessionAffinityRouteRules(t *testing.T) {
t.Run("without primary cookie", func(t *testing.T) {
canary := newTestGatewayAPICanary()
mocks := newFixture(canary)
cookieKey := "flagger-cookie"
@ -339,6 +353,7 @@ func TestGatewayAPIRouter_getSessionAffinityRouteRules(t *testing.T) {
assert.True(t, found)
rules, err = router.getSessionAffinityRouteRules(canary, 0, weightedRouteRule)
require.NoError(t, err)
assert.Empty(t, canary.Status.SessionAffinityCookie)
assert.Contains(t, canary.Status.PreviousSessionAffinityCookie, cookieKey)
@ -353,6 +368,116 @@ func TestGatewayAPIRouter_getSessionAffinityRouteRules(t *testing.T) {
assert.NotNil(t, headerModifier)
assert.Equal(t, string(headerModifier.Add[0].Name), setCookieHeader)
assert.Equal(t, headerModifier.Add[0].Value, fmt.Sprintf("%s; %s=%d", canary.Status.PreviousSessionAffinityCookie, maxAgeAttr, -1))
})
t.Run("with primary cookie", func(t *testing.T) {
canary := newTestGatewayAPICanary()
mocks := newFixture(canary)
canaryCookieKey := "canary-flagger-cookie"
primaryCookieKey := "primary-flagger-cookie"
canary.Spec.Analysis.Interval = "15s"
canary.Spec.Analysis.SessionAffinity = &flaggerv1.SessionAffinity{
CookieName: canaryCookieKey,
PrimaryCookieName: primaryCookieKey,
MaxAge: 300,
}
router := &GatewayAPIRouter{
gatewayAPIClient: mocks.meshClient,
kubeClient: mocks.kubeClient,
logger: mocks.logger,
}
_, pSvcName, cSvcName := canary.GetServiceNames()
weightedRouteRule := &v1.HTTPRouteRule{
BackendRefs: []v1.HTTPBackendRef{
{
BackendRef: router.makeBackendRef(pSvcName, initialPrimaryWeight, canary.Spec.Service.Port),
},
{
BackendRef: router.makeBackendRef(cSvcName, initialCanaryWeight, canary.Spec.Service.Port),
},
},
}
rules, err := router.getSessionAffinityRouteRules(canary, 10, weightedRouteRule)
require.NoError(t, err)
assert.Equal(t, len(rules), 3)
assert.True(t, strings.HasPrefix(canary.Status.SessionAffinityCookie, canaryCookieKey))
canaryStickyRule := rules[0]
cookieMatch := canaryStickyRule.Matches[0].Headers[0]
assert.Equal(t, *cookieMatch.Type, v1.HeaderMatchRegularExpression)
assert.Equal(t, string(cookieMatch.Name), cookieHeader)
assert.Contains(t, cookieMatch.Value, canaryCookieKey)
assert.Equal(t, len(canaryStickyRule.BackendRefs), 2)
for _, backendRef := range canaryStickyRule.BackendRefs {
if string(backendRef.BackendRef.Name) == pSvcName {
assert.Equal(t, *backendRef.BackendRef.Weight, int32(0))
}
if string(backendRef.BackendRef.Name) == cSvcName {
assert.Equal(t, *backendRef.BackendRef.Weight, int32(100))
}
}
primaryStickyRule := rules[1]
cookieMatch = primaryStickyRule.Matches[0].Headers[0]
assert.Equal(t, *cookieMatch.Type, v1.HeaderMatchRegularExpression)
assert.Equal(t, string(cookieMatch.Name), cookieHeader)
assert.Contains(t, cookieMatch.Value, primaryCookieKey)
assert.Equal(t, len(primaryStickyRule.BackendRefs), 2)
for _, backendRef := range primaryStickyRule.BackendRefs {
if string(backendRef.BackendRef.Name) == pSvcName {
assert.Equal(t, *backendRef.BackendRef.Weight, int32(100))
}
if string(backendRef.BackendRef.Name) == cSvcName {
assert.Equal(t, *backendRef.BackendRef.Weight, int32(0))
}
}
weightedRule := rules[2]
var c int
for _, backendRef := range weightedRule.BackendRefs {
if string(backendRef.Name) == cSvcName {
c += 1
filter := backendRef.Filters[0]
assert.Equal(t, filter.Type, v1.HTTPRouteFilterResponseHeaderModifier)
assert.NotNil(t, filter.ResponseHeaderModifier)
assert.Equal(t, string(filter.ResponseHeaderModifier.Add[0].Name), setCookieHeader)
assert.Equal(t, filter.ResponseHeaderModifier.Add[0].Value, fmt.Sprintf("%s; %s=%d", canary.Status.SessionAffinityCookie, maxAgeAttr, 300))
}
if string(backendRef.Name) == pSvcName {
c += 1
filter := backendRef.Filters[0]
assert.Equal(t, filter.Type, v1.HTTPRouteFilterResponseHeaderModifier)
assert.NotNil(t, filter.ResponseHeaderModifier)
assert.Equal(t, string(filter.ResponseHeaderModifier.Add[0].Name), setCookieHeader)
assert.Contains(t, filter.ResponseHeaderModifier.Add[0].Value, canary.Spec.Analysis.SessionAffinity.PrimaryCookieName)
interval, err := time.ParseDuration(canary.Spec.Analysis.Interval)
require.NoError(t, err)
assert.Contains(t, filter.ResponseHeaderModifier.Add[0].Value, fmt.Sprintf("%s=%d", maxAgeAttr, int(interval.Seconds())))
}
}
assert.Equal(t, 2, c)
rules, err = router.getSessionAffinityRouteRules(canary, 0, weightedRouteRule)
require.NoError(t, err)
assert.Empty(t, canary.Status.SessionAffinityCookie)
assert.Contains(t, canary.Status.PreviousSessionAffinityCookie, canaryCookieKey)
canaryStickyRule = rules[0]
cookieMatch = canaryStickyRule.Matches[0].Headers[0]
assert.Equal(t, *cookieMatch.Type, v1.HeaderMatchRegularExpression)
assert.Equal(t, string(cookieMatch.Name), cookieHeader)
assert.Contains(t, cookieMatch.Value, canaryCookieKey)
assert.Equal(t, canaryStickyRule.Filters[0].Type, v1.HTTPRouteFilterResponseHeaderModifier)
headerModifier := canaryStickyRule.Filters[0].ResponseHeaderModifier
assert.NotNil(t, headerModifier)
assert.Equal(t, string(headerModifier.Add[0].Name), setCookieHeader)
assert.Equal(t, headerModifier.Add[0].Value, fmt.Sprintf("%s; %s=%d", canary.Status.PreviousSessionAffinityCookie, maxAgeAttr, -1))
})
}
func TestGatewayAPIRouter_makeFilters(t *testing.T) {

View File

@ -23,10 +23,6 @@ import (
"slices"
"strings"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
"github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1"
istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1"
clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"go.uber.org/zap"
@ -34,6 +30,11 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
"github.com/fluxcd/flagger/pkg/apis/gatewayapi/v1beta1"
istiov1beta1 "github.com/fluxcd/flagger/pkg/apis/istio/v1beta1"
clientset "github.com/fluxcd/flagger/pkg/client/clientset/versioned"
)
var (
@ -200,16 +201,26 @@ func (gwr *GatewayAPIV1Beta1Router) Reconcile(canary *flaggerv1.Canary) error {
}
if httpRoute != nil {
// Preserve the existing annotations added by other controllers such as AWS Gateway API Controller.
mergedAnnotations := newMetadata.Annotations
for key, val := range httpRoute.Annotations {
if _, ok := mergedAnnotations[key]; !ok {
mergedAnnotations[key] = val
}
}
// Compare the existing HTTPRoute spec and metadata with the desired state.
// If there are differences, update the HTTPRoute object.
specDiff := cmp.Diff(
httpRoute.Spec, httpRouteSpec,
ignoreCmpOptions...,
)
labelsDiff := cmp.Diff(newMetadata.Labels, httpRoute.Labels, cmpopts.EquateEmpty())
annotationsDiff := cmp.Diff(newMetadata.Annotations, httpRoute.Annotations, cmpopts.EquateEmpty())
annotationsDiff := cmp.Diff(mergedAnnotations, httpRoute.Annotations, cmpopts.EquateEmpty())
if (specDiff != "" && httpRoute.Name != "") || labelsDiff != "" || annotationsDiff != "" {
hrClone := httpRoute.DeepCopy()
hrClone.Spec = httpRouteSpec
hrClone.ObjectMeta.Annotations = newMetadata.Annotations
hrClone.ObjectMeta.Annotations = mergedAnnotations
hrClone.ObjectMeta.Labels = newMetadata.Labels
_, err := gwr.gatewayAPIClient.GatewayapiV1beta1().HTTPRoutes(hrNamespace).
Update(context.TODO(), hrClone, metav1.UpdateOptions{})

View File

@ -55,6 +55,17 @@ func TestGatewayAPIV1Beta1Router_Reconcile(t *testing.T) {
require.Equal(t, len(backendRefs), 2)
assert.Equal(t, int32(100), *backendRefs[0].Weight)
assert.Equal(t, int32(0), *backendRefs[1].Weight)
// assert that http route annotations injected by the networking controller is preserved.
httpRoute.Annotations["foo"] = "bar"
_, err = router.gatewayAPIClient.GatewayapiV1beta1().HTTPRoutes("default").Update(context.TODO(), httpRoute, metav1.UpdateOptions{})
require.NoError(t, err)
err = router.Reconcile(canary)
require.NoError(t, err)
httpRoute, err = router.gatewayAPIClient.GatewayapiV1beta1().HTTPRoutes("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, httpRoute.Annotations["foo"], "bar")
}
func TestGatewayAPIV1Beta1Router_Routes(t *testing.T) {

127
pkg/router/knative.go Normal file
View File

@ -0,0 +1,127 @@
package router
import (
"context"
"fmt"
"slices"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
"go.uber.org/zap"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
serving "knative.dev/serving/pkg/apis/serving/v1"
knative "knative.dev/serving/pkg/client/clientset/versioned"
)
type KnativeRouter struct {
knativeClient knative.Interface
logger *zap.SugaredLogger
}
func (kr *KnativeRouter) Reconcile(canary *flaggerv1.Canary) error {
service, err := kr.knativeClient.ServingV1().Services(canary.Namespace).Get(context.TODO(), canary.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("Knative Service %s.%s get query error: %w", canary.Spec.TargetRef.Name, canary.Namespace, err)
}
if _, ok := service.Annotations["flagger.app/primary-revision"]; !ok {
if service.Annotations == nil {
service.Annotations = make(map[string]string)
}
service.Annotations["flagger.app/primary-revision"] = service.Status.LatestCreatedRevisionName
_, err = kr.knativeClient.ServingV1().Services(canary.Namespace).Update(context.TODO(), service, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("Knative Service %s.%s update query error: %w", canary.Spec.TargetRef.Name, canary.Namespace, err)
}
kr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
Infof("Knative Service %s.%s updated", service.Name, service.Namespace)
}
return nil
}
func (kr *KnativeRouter) SetRoutes(cd *flaggerv1.Canary, primaryWeight int, canaryWeight int, mirrored bool) error {
service, err := kr.knativeClient.ServingV1().Services(cd.Namespace).Get(context.TODO(), cd.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("Knative Service %s.%s get query error: %w", cd.Spec.TargetRef.Name, cd.Namespace, err)
}
primaryName, exists := service.Annotations["flagger.app/primary-revision"]
if !exists {
return fmt.Errorf("Knative Service %s.%s annotation not found", cd.Spec.TargetRef.Name, cd.Namespace)
}
canaryPercent := int64(canaryWeight)
primaryPercent := int64(primaryWeight)
latestRevision := true
traffic := []serving.TrafficTarget{
{
LatestRevision: &latestRevision,
Percent: &canaryPercent,
},
{
RevisionName: primaryName,
Percent: &primaryPercent,
},
}
service.Spec.Traffic = traffic
service, err = kr.knativeClient.ServingV1().Services(cd.Namespace).Update(context.TODO(), service, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("Knative Service %s.%s update query error %w", cd.Spec.TargetRef.Name, cd.Namespace, err)
}
return nil
}
func (kr *KnativeRouter) GetRoutes(cd *flaggerv1.Canary) (primaryWeight int, canaryWeight int, mirrored bool, error error) {
service, err := kr.knativeClient.ServingV1().Services(cd.Namespace).Get(context.TODO(), cd.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
error = fmt.Errorf("service %s.%s get query error: %w", cd.Spec.TargetRef.Name, cd.Namespace, err)
return
}
primaryName, exists := service.Annotations["flagger.app/primary-revision"]
if !exists {
error = fmt.Errorf("service %s.%s annotation not found", cd.Spec.TargetRef.Name, cd.Namespace)
return
}
canaryRevisionIdx := slices.IndexFunc(service.Status.Traffic, func(target serving.TrafficTarget) bool {
return *target.LatestRevision
})
primaryRevisionIdx := slices.IndexFunc(service.Status.Traffic, func(target serving.TrafficTarget) bool {
return target.RevisionName == primaryName
})
if canaryRevisionIdx == -1 || primaryRevisionIdx == -1 {
error = fmt.Errorf("Knative Service %s.%s traffic spec invalid", cd.Spec.TargetRef.Name, cd.Namespace)
return
}
if service.Status.Traffic[primaryRevisionIdx].Percent == nil {
error = fmt.Errorf("Knative Service %s.%s primary revision traffic percent does not exist", cd.Spec.TargetRef.Name, cd.Namespace)
return
}
if service.Status.Traffic[canaryRevisionIdx].Percent == nil {
error = fmt.Errorf("Knative Service %s.%s canary revision traffic percent does not exist", cd.Spec.TargetRef.Name, cd.Namespace)
return
}
return int(*service.Status.Traffic[primaryRevisionIdx].Percent), int(*service.Status.Traffic[canaryRevisionIdx].Percent), false, nil
}
func (kr *KnativeRouter) Finalize(canary *flaggerv1.Canary) error {
service, err := kr.knativeClient.ServingV1().Services(canary.Namespace).Get(context.TODO(), canary.Spec.TargetRef.Name, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("Knative Service %s.%s get query error: %w", canary.Spec.TargetRef.Name, canary.Namespace, err)
}
if _, ok := service.Annotations["flagger.app/primary-revision"]; ok {
delete(service.Annotations, "flagger.app/primary-revision")
_, err = kr.knativeClient.ServingV1().Services(canary.Namespace).Update(context.TODO(), service, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("Knative Service %s.%s update query error: %w", canary.Spec.TargetRef.Name, canary.Namespace, err)
}
kr.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
Infof("Knative Service %s.%s updated", service.Name, service.Namespace)
}
return nil
}

View File

@ -0,0 +1,96 @@
package router
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
serving "knative.dev/serving/pkg/apis/serving/v1"
)
func TestKnativeRouter_Reconcile(t *testing.T) {
canary := newTestKnativeCanary()
mocks := newFixture(canary)
router := &KnativeRouter{
knativeClient: mocks.knativeClient,
logger: mocks.logger,
}
err := router.Reconcile(canary)
require.NoError(t, err)
service, err := mocks.knativeClient.ServingV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, "podinfo-00001", service.Annotations["flagger.app/primary-revision"])
}
func TestKnativeRouter_SetRoutes(t *testing.T) {
canary := newTestKnativeCanary()
mocks := newFixture(canary)
router := &KnativeRouter{
knativeClient: mocks.knativeClient,
logger: mocks.logger,
}
// error when annotation is not set
err := router.SetRoutes(canary, 10, 90, false)
require.Error(t, err)
err = router.Reconcile(canary)
require.NoError(t, err)
err = router.SetRoutes(canary, 10, 90, false)
require.NoError(t, err)
service, err := mocks.knativeClient.ServingV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
assert.Len(t, service.Spec.Traffic, 2)
assert.Equal(t, *service.Spec.Traffic[0].LatestRevision, true)
assert.Equal(t, *service.Spec.Traffic[0].Percent, int64(90))
assert.Equal(t, service.Spec.Traffic[1].RevisionName, "podinfo-00001")
assert.Equal(t, *service.Spec.Traffic[1].Percent, int64(10))
}
func TestKnativeRouter_GetRoutes(t *testing.T) {
canary := newTestKnativeCanary()
mocks := newFixture(canary)
router := &KnativeRouter{
knativeClient: mocks.knativeClient,
logger: mocks.logger,
}
// error when annotation is not set
_, _, _, err := router.GetRoutes(canary)
require.Error(t, err)
err = router.Reconcile(canary)
require.NoError(t, err)
service, err := mocks.knativeClient.ServingV1().Services("default").Get(context.TODO(), "podinfo", metav1.GetOptions{})
require.NoError(t, err)
canaryPercent := int64(90)
primaryPercent := int64(10)
latestRevision := true
service.Status.Traffic = []serving.TrafficTarget{
{
LatestRevision: &latestRevision,
Percent: &canaryPercent,
},
{
RevisionName: "podinfo-00001",
Percent: &primaryPercent,
},
}
_, err = mocks.knativeClient.ServingV1().Services("default").Update(context.TODO(), service, metav1.UpdateOptions{})
require.NoError(t, err)
pWeight, cWeight, _, err := router.GetRoutes(canary)
require.NoError(t, err)
assert.Equal(t, pWeight, 10)
assert.Equal(t, cWeight, 90)
}

View File

@ -113,6 +113,9 @@ func (c *KubernetesDefaultRouter) reconcileService(canary *flaggerv1.Canary, nam
},
},
}
if canary.Spec.Service.Headless {
svcSpec.ClusterIP = "None"
}
if v := canary.Spec.Service.AppProtocol; v != "" {
svcSpec.Ports[0].AppProtocol = &v

View File

@ -34,6 +34,8 @@ import (
func TestServiceRouter_Create(t *testing.T) {
mocks := newFixture(nil)
mocks.canary.Spec.Service.Headless = true
router := &KubernetesDefaultRouter{
kubeClient: mocks.kubeClient,
flaggerClient: mocks.flaggerClient,
@ -53,11 +55,13 @@ func TestServiceRouter_Create(t *testing.T) {
assert.Equal(t, &appProtocol, canarySvc.Spec.Ports[0].AppProtocol)
assert.Equal(t, "http", canarySvc.Spec.Ports[0].Name)
assert.Equal(t, int32(9898), canarySvc.Spec.Ports[0].Port)
assert.Equal(t, "None", canarySvc.Spec.ClusterIP)
primarySvc, err := mocks.kubeClient.CoreV1().Services("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, "http", primarySvc.Spec.Ports[0].Name)
assert.Equal(t, int32(9898), primarySvc.Spec.Ports[0].Port)
assert.Equal(t, "None", primarySvc.Spec.ClusterIP)
}
func TestServiceRouter_Update(t *testing.T) {

View File

@ -21,11 +21,15 @@ import (
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
serving "knative.dev/serving/pkg/apis/serving/v1"
knative "knative.dev/serving/pkg/client/clientset/versioned"
fakeKnative "knative.dev/serving/pkg/client/clientset/versioned/fake"
appmesh "github.com/fluxcd/flagger/pkg/apis/appmesh"
flaggerv1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1"
@ -43,6 +47,7 @@ type fixture struct {
appmeshCanary *flaggerv1.Canary
ingressCanary *flaggerv1.Canary
kubeClient kubernetes.Interface
knativeClient knative.Interface
meshClient clientset.Interface
flaggerClient clientset.Interface
logger *zap.SugaredLogger
@ -83,6 +88,7 @@ func newFixture(c *flaggerv1.Canary) fixture {
kubeClient: kubeClient,
meshClient: meshClient,
flaggerClient: flaggerClient,
knativeClient: fakeKnative.NewSimpleClientset(newTestKnativeService()),
logger: logger,
}
}
@ -126,6 +132,39 @@ func newTestApisixRoute() *a6v2.ApisixRoute {
return ar
}
func newTestKnativeService() *serving.Service {
s := &serving.Service{
TypeMeta: metav1.TypeMeta{APIVersion: serving.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Name: "podinfo",
Namespace: "default",
},
Spec: serving.ServiceSpec{
ConfigurationSpec: serving.ConfigurationSpec{
Template: serving.RevisionTemplateSpec{
Spec: serving.RevisionSpec{
PodSpec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "podinfo",
Image: "quay.io/stefanprodan/podinfo:1.2.0",
},
},
},
},
},
},
},
Status: serving.ServiceStatus{
ConfigurationStatusFields: serving.ConfigurationStatusFields{
LatestCreatedRevisionName: "podinfo-00001",
},
},
}
return s
}
func newTestCanary() *flaggerv1.Canary {
cd := &flaggerv1.Canary{
TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()},
@ -577,3 +616,39 @@ func newTestGatewayAPICanary() *flaggerv1.Canary {
}
return cd
}
func newTestKnativeCanary() *flaggerv1.Canary {
cd := &flaggerv1.Canary{
TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "podinfo",
},
Spec: flaggerv1.CanarySpec{
Provider: "knative",
TargetRef: flaggerv1.LocalObjectReference{
Name: "podinfo",
APIVersion: "serving.knative.dev/v1",
Kind: "Service",
},
Analysis: &flaggerv1.CanaryAnalysis{
Threshold: 10,
StepWeight: 10,
MaxWeight: 50,
Metrics: []flaggerv1.CanaryMetric{
{
Name: "request-success-rate",
Threshold: 99,
Interval: "1m",
},
{
Name: "request-duration",
Threshold: 500,
Interval: "1m",
},
},
},
},
}
return cd
}

View File

@ -16,5 +16,5 @@ limitations under the License.
package version
var VERSION = "1.39.0"
var VERSION = "1.41.0"
var REVISION = "unknown"

View File

@ -44,7 +44,8 @@ spec:
maxWeight: 50
stepWeight: 10
sessionAffinity:
cookieName: flagger-cookie
cookieName: canary-flagger-cookie
primaryCookieName: primary-flagger-cookie
metrics:
- name: error-rate
templateRef:
@ -104,7 +105,8 @@ until ${ok}; do
done
echo '>>> Verifying session affinity'
if ! URL=http://localhost:8888 HOST=www.example.com VERSION=6.1.0 COOKIE_NAME=flagger-cookie \
if ! URL=http://localhost:8888 HOST=www.example.com CANARY_VERSION=6.1.0 \
CANARY_COOKIE_NAME=canary-flagger-cookie PRIMARY_VERSION=6.0.4 PRIMARY_COOKIE_NAME=primary-flagger-cookie \
go run ${REPO_ROOT}/test/gatewayapi/verify_session_affinity.go; then
echo "failed to verify session affinity"
exit $?

View File

@ -2,7 +2,7 @@ package main
import (
"fmt"
"io/ioutil"
"io"
"log"
"net/http"
"os"
@ -11,30 +11,39 @@ import (
"time"
)
var c = make(chan string, 1)
var mu sync.Mutex
var try = true
// channel for canary cookie
var cc = make(chan string, 1)
// channel for primary cookie
var pc = make(chan string, 1)
var timeout = time.Second * 10
func main() {
url := os.Getenv("URL")
host := os.Getenv("HOST")
version := os.Getenv("VERSION")
cookieName := os.Getenv("COOKIE_NAME")
canaryVersion := os.Getenv("CANARY_VERSION")
canaryCookieName := os.Getenv("CANARY_COOKIE_NAME")
primaryVersion := os.Getenv("PRIMARY_VERSION")
primaryCookieName := os.Getenv("PRIMARY_COOKIE_NAME")
// Generate traffic
for i := 0; i < 10; i++ {
go tryUntilCanaryIsHit(url, host, version, cookieName)
}
go tryUntilWorkloadIsHit(url, host, canaryVersion, canaryCookieName, cc)
go tryUntilWorkloadIsHit(url, host, primaryVersion, primaryCookieName, pc)
wg := &sync.WaitGroup{}
wg.Add(2)
go verifySessionAffinity(cc, wg, url, host, primaryVersion)
go verifySessionAffinity(pc, wg, url, host, canaryVersion)
wg.Wait()
log.Println("✔ successfully verified session affinity")
}
func verifySessionAffinity(cc chan string, wg *sync.WaitGroup, url, host, wrongVersion string) {
select {
// If we receive a cookie, then try to verify that we are always routed to the
// Canary deployment based on the cookie.
case cookie := <-c:
mu.Lock()
try = false
mu.Unlock()
case cookie := <-cc:
for i := 0; i < 5; i++ {
headers := map[string]string{
"Cookie": cookie,
@ -43,15 +52,15 @@ func main() {
if err != nil {
log.Fatalf("failed to send request to verify cookie based routing: %v", err)
}
if !strings.Contains(body, version) {
log.Fatalf("received response from primary deployment instead of canary deployment")
if strings.Contains(body, wrongVersion) {
log.Fatalf("received response from the wrong deployment")
}
}
wg.Done()
case <-time.After(timeout):
log.Fatal("timed out waiting for workload hit")
}
log.Println("✔ successfully verified session affinity")
case <-time.After(timeout):
log.Fatal("timed out waiting for canary hit")
}
}
// sendRequest sends a request to the URL with the provided host and headers.
@ -74,7 +83,7 @@ func sendRequest(url, host string, headers map[string]string) (string, []*http.C
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil, err
}
@ -82,17 +91,10 @@ func sendRequest(url, host string, headers map[string]string) (string, []*http.C
return string(body), resp.Cookies(), nil
}
// tryUntilCanaryIsHit is a recursive function that tries to send request and
// tryUntilWorkloadIsHit is a recursive function that tries to send request and
// either sends the cookie back to the main thread (if received) or re-sends
// the request.
func tryUntilCanaryIsHit(url, host, version, cookieName string) {
mu.Lock()
if !try {
mu.Unlock()
return
}
mu.Unlock()
func tryUntilWorkloadIsHit(url, host, version, cookieName string, c chan string) {
body, cookies, err := sendRequest(url, host, nil)
if err != nil {
log.Printf("warning: failed to send request: %s", err)
@ -105,6 +107,5 @@ func tryUntilCanaryIsHit(url, host, version, cookieName string) {
}
}
tryUntilCanaryIsHit(url, host, version, cookieName)
return
tryUntilWorkloadIsHit(url, host, version, cookieName, c)
}

41
test/knative/init.sh Executable file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -o errexit
REPO_ROOT=$(git rev-parse --show-toplevel)
echo '>>> Delete test namespace'
kubectl delete namespace test --ignore-not-found=true --wait=true
echo '>>> Creating test namespace'
kubectl create namespace test
echo '>>> Installing the load tester'
kubectl apply -k ${REPO_ROOT}/kustomize/tester
kubectl -n test rollout status deployment/flagger-loadtester
echo '>>> Deploy Knative Service'
cat <<EOF | kubectl apply -f -
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: podinfo
namespace: test
spec:
template:
spec:
containers:
- image: ghcr.io/stefanprodan/podinfo:6.0.0
ports:
- containerPort: 9898
protocol: TCP
command:
- ./podinfo
- --port=9898
- --port-metrics=9797
- --grpc-port=9999
- --grpc-service-name=podinfo
- --level=info
- --random-delay=false
- --random-error=false
EOF

26
test/knative/install.sh Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -o errexit
KNATIVE_VER="1.17.0"
REPO_ROOT=$(git rev-parse --show-toplevel)
mkdir -p ${REPO_ROOT}/bin
echo '>>> Installing Knative'
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v${KNATIVE_VER}/serving-crds.yaml
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v${KNATIVE_VER}/serving-core.yaml
kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v${KNATIVE_VER}/kourier.yaml
kubectl patch configmap/config-network \
--namespace knative-serving \
--type merge \
--patch '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}'
kubectl -n knative-serving rollout status deployment
kubectl -n kourier-system rollout status deployment
echo '>>> Installing Flagger'
kubectl apply -k ${REPO_ROOT}/kustomize/knative
kubectl -n flagger-system set image deployment/flagger flagger=test/flagger:latest
kubectl -n flagger-system rollout status deployment/flagger

11
test/knative/run.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -o errexit
REPO_ROOT=$(git rev-parse --show-toplevel)
DIR="$(cd "$(dirname "$0")" && pwd)"
"$DIR"/install.sh
"$DIR"/init.sh
"$DIR"/test-canary.sh

159
test/knative/test-canary.sh Executable file
View File

@ -0,0 +1,159 @@
#!/usr/bin/env bash
# This script runs e2e tests for Canary initialization, analysis and promotion
# Prerequisites: Kubernetes Kind and Knative Serving
set -o errexit
REPO_ROOT=$(git rev-parse --show-toplevel)
cat <<EOF | kubectl apply -f -
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: podinfo
namespace: test
spec:
provider: knative
targetRef:
apiVersion: serving.knative.dev/v1
kind: Service
name: podinfo
progressDeadlineSeconds: 60
analysis:
interval: 15s
threshold: 15
maxWeight: 50
stepWeight: 10
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 1m
webhooks:
- name: load-test
url: http://flagger-loadtester.test/
timeout: 5s
metadata:
type: cmd
cmd: "hey -z 1m -q 5 -c 2 http://podinfo.test"
logCmdOutput: "true"
EOF
echo '>>> Waiting for primary to be ready'
retries=50
count=0
ok=false
until ${ok}; do
kubectl -n test get canary/podinfo | grep 'Initialized' && ok=true || ok=false
sleep 5
count=$(($count + 1))
if [[ ${count} -eq ${retries} ]]; then
kubectl -n flagger-system logs deployment/flagger
echo "No more retries left"
exit 1
fi
done
kubectl -n test get services.serving podinfo -oyaml | grep 'flagger.app/primary-revision: podinfo-00001'
echo '✔ Canary initialization test passed'
echo '>>> Triggering canary deployment'
kubectl -n test patch services.serving podinfo --type=json -p '[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "ghcr.io/stefanprodan/podinfo:6.0.1"}]'
echo '>>> Waiting for canary promotion'
retries=50
count=0
ok=false
until ${ok}; do
kubectl -n test get services.serving podinfo -oyaml | grep 'flagger.app/primary-revision: podinfo-00002' && ok=true || ok=false
sleep 10
kubectl -n flagger-system logs deployment/flagger --tail 1
count=$(($count + 1))
if [[ ${count} -eq ${retries} ]]; then
kubectl -n flagger-system logs deployment/flagger
kubectl -n test get services.serving podinfo -oyaml
echo "No more retries left"
exit 1
fi
done
echo '>>> Waiting for canary finalization'
retries=50
count=0
ok=false
until ${ok}; do
kubectl -n test get canary/podinfo | grep 'Succeeded' && ok=true || ok=false
sleep 5
count=$(($count + 1))
if [[ ${count} -eq ${retries} ]]; then
kubectl -n flagger-system logs deployment/flagger
echo "No more retries left"
exit 1
fi
done
echo '✔ Canary promotion test passed'
cat <<EOF | kubectl apply -f -
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: podinfo
namespace: test
spec:
provider: knative
targetRef:
apiVersion: serving.knative.dev/v1
kind: Service
name: podinfo
progressDeadlineSeconds: 60
analysis:
interval: 15s
threshold: 15
maxWeight: 50
stepWeight: 10
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 1m
webhooks:
- name: load-test
url: http://flagger-loadtester.test/
timeout: 5s
metadata:
type: cmd
cmd: "hey -z 1m -q 5 -c 2 http://podinfo.test/delay/1"
logCmdOutput: "true"
EOF
echo '>>> Triggering canary deployment'
kubectl -n test patch services.serving podinfo --type=json -p '[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "ghcr.io/stefanprodan/podinfo:6.0.2"}]'
echo '>>> Waiting for canary rollback'
retries=50
count=0
ok=false
until ${ok}; do
kubectl -n test get canary/podinfo | grep 'Failed' && ok=true || ok=false
sleep 10
kubectl -n flagger-system logs deployment/flagger --tail 1
count=$(($count + 1))
if [[ ${count} -eq ${retries} ]]; then
kubectl -n flagger-system logs deployment/flagger
echo "No more retries left"
exit 1
fi
done
echo '✔ Canary rollback test passed'