Compare commits

..

22 Commits
main ... v1.2.2

Author SHA1 Message Date
Hidde Beydals 644100a639
Merge pull request #678 from fluxcd/release-v1.2.2
Release v1.2.2
2023-12-11 15:35:30 +01:00
Hidde Beydals aaacfc4cd8
Release v1.2.2
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
2023-12-11 15:28:21 +01:00
Hidde Beydals 9066a87e56
Add changelog entry for v1.2.2
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
2023-12-11 15:24:38 +01:00
Hidde Beydals a93a06a148
Merge pull request #677 from fluxcd/backport-676-to-release/v1.2.x
[release/v1.2.x] Update dependencies
2023-12-11 15:21:03 +01:00
Stefan Prodan 4e60e5c7e6
Update dependencies
- github.com/fluxcd/pkg/runtime v0.43.2
- github.com/fluxcd/cli-utils v0.36.0-flux.2
- github.com/xanzy/go-gitlab v0.95.1

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
(cherry picked from commit 74bd37a992)
2023-12-11 14:55:01 +01:00
Hidde Beydals 00cc7ce67d
Merge pull request #675 from fluxcd/backport-672-to-release/v1.2.x
[release/v1.2.x] build(deps): bump the ci group with 1 update
2023-12-11 14:20:10 +01:00
dependabot[bot] 90c21fddd4 build(deps): bump the ci group with 1 update
Bumps the ci group with 1 update: [actions/setup-go](https://github.com/actions/setup-go).

- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](93397bea11...0c52d547c9)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 879f9576e2)
2023-12-11 08:32:31 +00:00
Hidde Beydals 07b3dfadeb
Merge pull request #674 from fluxcd/backport-673-to-release/v1.2.x
[release/v1.2.x] build: update Alpine to 3.19
2023-12-11 09:25:29 +01:00
Hidde Beydals a1236db503 build: update Alpine to 3.19
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
(cherry picked from commit 40ba471d6b)
2023-12-11 08:20:05 +00:00
Hidde Beydals e2b848e012
Merge pull request #670 from fluxcd/release-v1.2.1
Release v1.2.1
2023-12-08 10:57:53 +01:00
Hidde Beydals 136bd28f1f
Release v1.2.1
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
2023-12-08 10:34:00 +01:00
Hidde Beydals d5572c1e2f
Add changelog entry for v1.2.1
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
2023-12-08 10:33:59 +01:00
Hidde Beydals 9101537b1a
Merge pull request #669 from fluxcd/backport-668-to-release/v1.2.x
[release/v1.2.x] Update dependencies
2023-12-08 09:45:00 +01:00
Hidde Beydals f8b9daef43 Update dependencies
- github.com/ktrysmt/go-bitbucket to v0.9.73
- google.golang.org/api to v0.153.0

Signed-off-by: Hidde Beydals <hidde@hhh.computer>
(cherry picked from commit b007f42139)
2023-12-08 08:30:31 +00:00
Hidde Beydals a548d3d5a1
Merge pull request #667 from fluxcd/backport-664-to-release/v1.2.x
[release/v1.2.x] Replace whilp/git-urls module by chainguard-dev/git-urls
2023-12-06 09:51:12 +01:00
Hector Fernandez 7eb260983c replace whilp/git-urls module by chainguard-dev/git-urls
Signed-off-by: Hector Fernandez <hector@chainguard.dev>
(cherry picked from commit 39a3853c5c)
2023-12-06 08:44:03 +00:00
Hidde Beydals daaee69d6d
Merge pull request #666 from fluxcd/backport-665-to-release/v1.2.x
[release/v1.2.x] Update Go to 1.21.x
2023-12-06 09:43:25 +01:00
Hidde Beydals 0d0db3eb81 Update `tonistiigi/xx` to 1.3.0
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
(cherry picked from commit c8d287e9c3)
2023-12-06 08:37:55 +00:00
Hidde Beydals 9aa2132245 Update Go to 1.21.x
As appears to be magically required by
`github.com/slok/go-http-metrics`.

Signed-off-by: Hidde Beydals <hidde@hhh.computer>
(cherry picked from commit f045217d70)
2023-12-06 08:37:55 +00:00
Sunny 7e2901b2a6
Merge pull request #658 from fluxcd/release-v1.2.0
Release v1.2.0
2023-12-06 00:02:09 +05:30
Sunny 3a22adb3a2
Release v1.2.0
Signed-off-by: Sunny <darkowlzz@protonmail.com>
2023-12-05 18:53:11 +05:30
Sunny 35ceca22e0
Add changelog entry for v1.2.0
Signed-off-by: Sunny <darkowlzz@protonmail.com>
2023-12-05 18:53:06 +05:30
136 changed files with 4636 additions and 8457 deletions

View File

@ -1,30 +1,16 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
labels: ["dependencies"]
schedule:
interval: "monthly"
groups:
go-deps:
patterns:
- "*"
allow:
- dependency-type: "direct"
ignore:
# Kubernetes deps are updated by fluxcd/pkg
- dependency-name: "k8s.io/*"
- dependency-name: "sigs.k8s.io/*"
- dependency-name: "github.com/go-logr/*"
# Flux APIs pkg are updated at release time
- dependency-name: "github.com/fluxcd/notification-controller/api"
- package-ecosystem: "github-actions"
directory: "/"
labels: ["area/ci", "dependencies"]
schedule:
# By default, this will be on a monday.
interval: "weekly"
groups:
# Group all updates together, so that they are all applied in a single PR.
# Grouped updates are currently in beta and is subject to change.
# xref: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups
ci:
patterns:
- "*"
schedule:
interval: "monthly"

15
.github/labels.yaml vendored
View File

@ -13,18 +13,3 @@
- name: backport:release/v1.1.x
description: To be backported to release/v1.1.x
color: '#ffd700'
- name: backport:release/v1.2.x
description: To be backported to release/v1.2.x
color: '#ffd700'
- name: backport:release/v1.3.x
description: To be backported to release/v1.3.x
color: '#ffd700'
- name: backport:release/v1.4.x
description: To be backported to release/v1.4.x
color: '#ffd700'
- name: backport:release/v1.5.x
description: To be backported to release/v1.5.x
color: '#ffd700'
- name: backport:release/v1.6.x
description: To be backported to release/v1.6.x
color: '#ffd700'

View File

@ -13,11 +13,11 @@ jobs:
if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name))
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs
uses: korthout/backport-action@436145e922f9561fc5ea157ff406f21af2d6b363 # v3.2.0
uses: korthout/backport-action@08bafb375e6e9a9a2b53a744b987e5d81a133191 # v2.1.1
# xref: https://github.com/korthout/backport-action#inputs
with:
# Use token to allow workflows to be triggered for the created PR

View File

@ -11,11 +11,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
with:
go-version: 1.24.x
go-version: 1.21.x
cache-dependency-path: |
**/go.sum
**/go.mod

View File

@ -12,14 +12,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Cache Docker layers
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
id: cache
with:
path: /tmp/.buildx-cache
@ -27,16 +27,18 @@ jobs:
restore-keys: |
${{ runner.os }}-buildx-ghcache-
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
with:
go-version: 1.24.x
go-version: 1.21.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Setup Kubernetes
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 # v1.8.0
with:
version: v0.20.0
cluster_name: kind
node_image: kindest/node:v1.27.3@sha256:3966ac761ae0136263ffdb6cfd4db23ef8a83cba8a463690e98317add2c9ba72
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@main
- name: Run tests

View File

@ -29,7 +29,7 @@ jobs:
packages: write # for pushing and signing container images.
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@main
- name: Prepare
@ -42,24 +42,24 @@ jobs:
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
- name: Setup QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: ghcr.io
username: fluxcdbot
password: ${{ secrets.GHCR_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
- name: Generate images meta
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: |
fluxcd/${{ env.CONTROLLER }}
@ -68,7 +68,7 @@ jobs:
type=raw,value=${{ steps.prep.outputs.VERSION }}
- name: Publish images
id: build-push
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0
with:
sbom: true
provenance: true
@ -79,7 +79,7 @@ jobs:
platforms: linux/amd64,linux/arm/v7,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
- uses: sigstore/cosign-installer@1fc5bd396d372bee37d608f955b336615edf79c8 # v3.2.0
- name: Sign images
env:
COSIGN_EXPERIMENTAL: 1
@ -92,14 +92,14 @@ jobs:
mkdir -p config/release
kustomize build ./config/crd > ./config/release/${{ env.CONTROLLER }}.crds.yaml
kustomize build ./config/manager > ./config/release/${{ env.CONTROLLER }}.deployment.yaml
- uses: anchore/sbom-action/download-syft@e11c554f704a0b820cbf8c51673f6945e0731532 # v0.20.0
- uses: anchore/sbom-action/download-syft@fd74a6fb98a204a1ad35bbfae0122c1a302ff88b # v0.15.0
- name: Create release and SBOM
id: run-goreleaser
if: startsWith(github.ref, 'refs/tags/v')
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0
with:
version: latest
args: release --clean --skip=validate
args: release --clean --skip-validate
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate SLSA metadata
@ -123,7 +123,7 @@ jobs:
id-token: write # for creating OIDC tokens for signing.
contents: write # for uploading attestations to GitHub releases.
if: startsWith(github.ref, 'refs/tags/v')
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
with:
provenance-name: "provenance.intoto.jsonl"
base64-subjects: "${{ needs.release.outputs.hashes }}"
@ -136,7 +136,7 @@ jobs:
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
if: startsWith(github.ref, 'refs/tags/v')
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
with:
image: ${{ needs.release.outputs.image_url }}
digest: ${{ needs.release.outputs.image_digest }}
@ -151,7 +151,7 @@ jobs:
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
if: startsWith(github.ref, 'refs/tags/v')
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
with:
image: ghcr.io/${{ needs.release.outputs.image_url }}
digest: ${{ needs.release.outputs.image_digest }}

View File

@ -18,9 +18,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Run FOSSA scan and upload build data
uses: fossa-contrib/fossa-action@3d2ef181b1820d6dcd1972f86a767d18167fa19b # v3.0.1
uses: fossa-contrib/fossa-action@cdc5065bcdee31a32e47d4585df72d66e8e941c2 # v3.0.0
with:
# FOSSA Push-Only API Token
fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de
@ -31,22 +31,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0
with:
go-version: 1.24.x
go-version: 1.21.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Initialize CodeQL
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/init@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
with:
languages: go
# xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# xref: https://codeql.github.com/codeql-query-help/go/
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/autobuild@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
uses: github/codeql-action/analyze@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4

View File

@ -17,8 +17,8 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: EndBug/label-sync@da00f2c11fdb78e4fae44adac2fdd713778ea3e8 # v2.3.2
with:
# Configuration file
config-file: |

View File

@ -23,7 +23,7 @@ release:
To verify the images and their provenance (SLSA level 3), please see the [security documentation](https://fluxcd.io/flux/security/).
changelog:
disable: true
skip: true
checksum:
extra_files:

View File

@ -2,281 +2,6 @@
All notable changes to this project are documented in this file.
## 1.6.0
**Release date:** 2025-05-27
This minor release comes with various bug fixes and improvements.
### Provider
The `azureeventhub` provider now supports workload identity both
at the controller and object levels. For object level, the
`.spec.serviceAccountName` field can be set to the name of a
service account in the same namespace that was configured with
a Managed Identity.
For object level to work, the controller feature gate
`ObjectLevelWorkloadIdentity` must be enabled. See a complete guide
[here](https://fluxcd.io/flux/integrations/azure/).
The `github` and `githubdispatch` providers now support authenticating
with a GitHub App. See docs
[here](https://fluxcd.io/flux/components/notification/providers/#github)
and
[here](https://fluxcd.io/flux/components/notification/providers/#github-dispatch).
For commit status providers it is now possible to define a custom
status string by defining a CEL expression in the `.spec.commitStatusExpr`
field. The variables `event`, `alert` and `provider` are available
for the CEL expression. See
[docs](https://fluxcd.io/flux/components/notification/providers/#custom-commit-status-messages).
### General updates
In addition, the Kubernetes dependencies have been updated to v1.33 and
various other controller dependencies have been updated to their latest
version. The controller is now built with Go 1.24.
Fixes:
- Fix Slack chat.postMessage error handling
[#1086](https://github.com/fluxcd/notification-controller/pull/1086)
- Fix pass 'certPool' to Gitea client on creation
[#1084](https://github.com/fluxcd/notification-controller/pull/1084)
- CrossNamespaceObjectReference: Fix MaxLength validation to kubernetes max size of 253
[#1108](https://github.com/fluxcd/notification-controller/pull/1108)
- Sanitize proxy error logging
[#1093](https://github.com/fluxcd/notification-controller/pull/1093)
Improvements:
- [RFC-0010] Workload Identity support for `azureeventhub` provider
[#1106](https://github.com/fluxcd/notification-controller/pull/1106)
[#1116](https://github.com/fluxcd/notification-controller/pull/1116)
[#1120](https://github.com/fluxcd/notification-controller/pull/1120)
[#1109](https://github.com/fluxcd/notification-controller/pull/1109)
[#1112](https://github.com/fluxcd/notification-controller/pull/1112)
- GitHub App authentication support for `github` and `githubdispatch`
[#1058](https://github.com/fluxcd/notification-controller/pull/1058)
- Support CEL expressions to construct commit statuses
[#1068](https://github.com/fluxcd/notification-controller/pull/1068)
- Add proxy support to `gitea` provider
[#1087](https://github.com/fluxcd/notification-controller/pull/1087)
- Various dependency updates
[#1101](https://github.com/fluxcd/notification-controller/pull/1101)
[#1119](https://github.com/fluxcd/notification-controller/pull/1119)
[#1118](https://github.com/fluxcd/notification-controller/pull/1118)
[#1113](https://github.com/fluxcd/notification-controller/pull/1113)
[#1104](https://github.com/fluxcd/notification-controller/pull/1104)
## 1.5.0
**Release date:** 2025-02-13
This minor release comes with various bug fixes and improvements.
### Alert
Now notification-controller also sends event metadata specified in Flux objects through
annotations. See [docs](https://fluxcd.io/flux/components/notification/alerts/#event-metadata-from-object-annotations).
Now notification-controller is also capable of updating Git commit statuses
from events about Kustomizations that consume OCIRepositories. See
[docs](https://fluxcd.io/flux/cheatsheets/oci-artifacts/#git-commit-status-updates).
### Receiver
The Receiver API now supports filtering the declared resources that
match a given Common Expression Language (CEL) expression. See
[docs](https://fluxcd.io/flux/components/notification/receivers/#filtering-reconciled-objects-with-cel).
In addition, the Kubernetes dependencies have been updated to v1.32.1 and
various other controller dependencies have been updated to their latest
version.
Fixes:
- Remove deprecated object metrics from controllers
[#997](https://github.com/fluxcd/notification-controller/pull/997)
- msteams notifier: adaptive cards full width
[#1017](https://github.com/fluxcd/notification-controller/pull/1017)
- fix: adding of duplicate commit statuses in gitlab
[#1010](https://github.com/fluxcd/notification-controller/pull/1010)
- Fix add missing return statement and a few style issues
[#1039](https://github.com/fluxcd/notification-controller/pull/1039)
Improvements:
- [RFC-0008] Custom Event Metadata from Annotations
[#1014](https://github.com/fluxcd/notification-controller/pull/1014)
- Add support for MetaOriginRevisionKey from the Event API
[#1018](https://github.com/fluxcd/notification-controller/pull/1018)
- Add subsection for Git providers supporting commit status updates
[#1019](https://github.com/fluxcd/notification-controller/pull/1019)
- Add support for Bearer Token authentication to Provider alertmanager
[#1021](https://github.com/fluxcd/notification-controller/pull/1021)
- Enforce namespace check on receiver
[#1022](https://github.com/fluxcd/notification-controller/pull/1022)
- Implement Receiver resource filtering with CEL
[#948](https://github.com/fluxcd/notification-controller/pull/948)
- Clarify gitlab provider usage
[#953](https://github.com/fluxcd/notification-controller/pull/953)
- Add involved object reference as annotations for the grafana provider
[#1040](https://github.com/fluxcd/notification-controller/pull/1040)
- Improvements after CEL resource filtering
[#1041](https://github.com/fluxcd/notification-controller/pull/1041)
- Various dependency updates
[#1002](https://github.com/fluxcd/notification-controller/pull/1002)
[#1016](https://github.com/fluxcd/notification-controller/pull/1016)
[#1023](https://github.com/fluxcd/notification-controller/pull/1023)
[#1025](https://github.com/fluxcd/notification-controller/pull/1025)
[#1027](https://github.com/fluxcd/notification-controller/pull/1027)
[#1032](https://github.com/fluxcd/notification-controller/pull/1032)
[#1036](https://github.com/fluxcd/notification-controller/pull/1036)
[#1037](https://github.com/fluxcd/notification-controller/pull/1037)
[#1042](https://github.com/fluxcd/notification-controller/pull/1042)
## 1.4.0
**Release date:** 2024-09-27
This minor release comes with various bug fixes and improvements.
MS Teams Provider has been updated to support MS Adaptive Card payloads.
This allows users to migrate from the deprecated
[Office 365 Connector for Incoming Webhooks](https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/)
to the new [Microsoft Teams Incoming Webhooks with Workflows](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498).
See the [Provider API documentation](https://fluxcd.io/flux/components/notification/providers/#microsoft-teams)
for more information. After getting the URL for the new Incoming Webhook Workflow,
update the secret used by the `msteams` Provider object with the new URL.
In addition, the Kubernetes dependencies have been updated to v1.31.1 and
various other controller dependencies have been updated to their latest
version. The controller is now built with Go 1.23.
Fixes:
- telegram notifier should escape with metadata key
[#829](https://github.com/fluxcd/notification-controller/pull/829)
- docs: use stringData for secret of GitHub PAT
[#873](https://github.com/fluxcd/notification-controller/pull/873)
- Fix incorrect use of format strings with the conditions package.
[#879](https://github.com/fluxcd/notification-controller/pull/879)
Improvements:
- New flag to disable detailed metrics for path
[#841](https://github.com/fluxcd/notification-controller/pull/841)
- Fix telegram test flake
[#894](https://github.com/fluxcd/notification-controller/pull/894)
- Build with Go 1.23
[#907](https://github.com/fluxcd/notification-controller/pull/907)
- Add MS Adaptive Card payload to msteams Provider
[#920](https://github.com/fluxcd/notification-controller/pull/920)
- Various dependency updates
[#845](https://github.com/fluxcd/notification-controller/pull/845)
[#855](https://github.com/fluxcd/notification-controller/pull/855)
[#854](https://github.com/fluxcd/notification-controller/pull/854)
[#857](https://github.com/fluxcd/notification-controller/pull/857)
[#865](https://github.com/fluxcd/notification-controller/pull/865)
[#866](https://github.com/fluxcd/notification-controller/pull/866)
[#905](https://github.com/fluxcd/notification-controller/pull/905)
[#903](https://github.com/fluxcd/notification-controller/pull/903)
[#912](https://github.com/fluxcd/notification-controller/pull/912)
[#925](https://github.com/fluxcd/notification-controller/pull/925)
[#931](https://github.com/fluxcd/notification-controller/pull/931)
[#932](https://github.com/fluxcd/notification-controller/pull/932)
[#933](https://github.com/fluxcd/notification-controller/pull/933)
[#934](https://github.com/fluxcd/notification-controller/pull/934)
## 1.3.0
**Release date:** 2024-05-06
This minor release comes with new features, improvements and bug fixes.
The `Receiver` API has been extended to support CDEvents,
for more information, please see the
[CDEvents Receiver API documentation](https://github.com/fluxcd/notification-controller/blob/release/v1.3.x/docs/spec/v1/receivers.md#cdevents).
Starting with this version, the controller allows grouping alerts for Alertmanager
by setting the `startsAt` label instead of `timestamp`. When sending alerts to
OpsGenie, the controller now sets the `severity` field to the alert's details.
In addition, the controller dependencies have been updated to Kubernetes v1.30
and controller-runtime v0.18. Various other dependencies have also been updated to
their latest version to patch upstream CVEs.
Lastly, the controller is now built with Go 1.22.
Improvements:
- Add CDEvent Receiver Support
[#772](https://github.com/fluxcd/notification-controller/pull/772)
- Add severity to opsgenie alerts
[#796](https://github.com/fluxcd/notification-controller/pull/796)
- Alertmanager: Change timestamp label to .StartsAt
[#795](https://github.com/fluxcd/notification-controller/pull/795)
- Use `password` as fallback for the Git provider `token` auth
[#790](https://github.com/fluxcd/notification-controller/pull/790)
- Add support for Bitbucket Context path
[#747](https://github.com/fluxcd/notification-controller/pull/747)
- Various dependency updates
[#816](https://github.com/fluxcd/notification-controller/pull/816)
[#814](https://github.com/fluxcd/notification-controller/pull/814)
[#813](https://github.com/fluxcd/notification-controller/pull/813)
[#810](https://github.com/fluxcd/notification-controller/pull/810)
[#809](https://github.com/fluxcd/notification-controller/pull/809)
[#787](https://github.com/fluxcd/notification-controller/pull/787)
[#783](https://github.com/fluxcd/notification-controller/pull/783)
[#763](https://github.com/fluxcd/notification-controller/pull/763)
Fixes:
- Sanitize provider data loaded from secret
[#789](https://github.com/fluxcd/notification-controller/pull/789)
- Fix timeout propagation for alerts
[#757](https://github.com/fluxcd/notification-controller/pull/757)
- Fix Telegram MarkdownV2 escaping
[#776](https://github.com/fluxcd/notification-controller/pull/776)
- Remove `genclient:Namespaced` tag
[#749](https://github.com/fluxcd/notification-controller/pull/749)
## 1.2.4
**Release date:** 2024-02-01
This patch release fixes various issues, updates the Kubernetes dependencies
to v1.28.6 and various other dependencies to their latest version to patch
upstream CVEs.
Improvements:
- Various dependency updates
[#727](https://github.com/fluxcd/notification-controller/pull/727)
[#726](https://github.com/fluxcd/notification-controller/pull/726)
[#721](https://github.com/fluxcd/notification-controller/pull/721)
[#718](https://github.com/fluxcd/notification-controller/pull/718)
[#707](https://github.com/fluxcd/notification-controller/pull/707)
[#695](https://github.com/fluxcd/notification-controller/pull/695)
Fixes:
- Fix BitBucket status update panic
[#722](https://github.com/fluxcd/notification-controller/pull/722)
- fix typo in docs/spec/v1beta3/providers.md
[#699](https://github.com/fluxcd/notification-controller/pull/699)
- fix(grafana-provider): replace ":" character in eventMetadata
[#703](https://github.com/fluxcd/notification-controller/pull/703)
- Remove old/incorrect API version usage
[#693](https://github.com/fluxcd/notification-controller/pull/693)
## 1.2.3
**Release date:** 2023-12-14
This patch release fixes various issues, most notably, the Provider v1beta3 API
backwards compatibility issue when `.spec.interval` was explicitly set in a
v1beta2 version of Provider.
Fixes:
- Exclude eventv1.MetaTokenKey from event metadata
[#686](https://github.com/fluxcd/notification-controller/pull/686)
- Add .spec.interval in v1beta3 Provider
[#683](https://github.com/fluxcd/notification-controller/pull/683)
- Remove URL syntax validation for provider address entirely
[#682](https://github.com/fluxcd/notification-controller/pull/682)
## 1.2.2
**Release date:** 2023-12-11

View File

@ -24,7 +24,7 @@ If any of the above dependencies are not present on your system, the first invoc
## How to run the test suite
Prerequisites:
- Go >= 1.24
- Go >= 1.17
You can run the test suite by simply doing:

View File

@ -1,9 +1,9 @@
ARG GO_VERSION=1.24
ARG XX_VERSION=1.6.1
ARG GO_VERSION=1.21
ARG XX_VERSION=1.3.0
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 the build utilities.
COPY --from=xx / /
@ -30,7 +30,7 @@ COPY internal/ internal/
ENV CGO_ENABLED=0
RUN xx-go build -trimpath -a -o notification-controller main.go
FROM alpine:3.21
FROM alpine:3.19
ARG TARGETPLATFORM

View File

@ -7,6 +7,4 @@ as listed in
https://github.com/fluxcd/flux2/blob/main/MAINTAINERS
In alphabetical order:
Somtochi Onyekwere, Independent <somtochionyekwere@gmail.com> (github: @somtochiama, slack: somtochiama)
Somtochi Onyekwere, Weaveworks <somtochi@weave.works> (github: @SomtochiAma, slack: somtoxhi)

View File

@ -2,14 +2,14 @@
IMG ?= fluxcd/notification-controller:latest
# Produce CRDs that work back to Kubernetes 1.16
CRD_OPTIONS ?= crd:crdVersions=v1
SOURCE_VER ?= v1.2.4
SOURCE_VER ?= v1.1.2
# Repository root based on Git metadata
REPOSITORY_ROOT := $(shell git rev-parse --show-toplevel)
BUILD_DIR := $(REPOSITORY_ROOT)/build
# API (doc) generation utilities
CONTROLLER_GEN_VERSION ?= v0.16.1
CONTROLLER_GEN_VERSION ?= v0.12.0
GEN_API_REF_DOCS_VERSION ?= e327d0730470cbd61b06300f81c5fcf91c23c113
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
@ -93,8 +93,8 @@ api-docs: gen-crd-api-reference-docs
# Run go mod tidy
tidy:
cd api; rm -f go.sum; go mod tidy -compat=1.24
rm -f go.sum; go mod tidy -compat=1.24
cd api; rm -f go.sum; go mod tidy -compat=1.20
rm -f go.sum; go mod tidy -compat=1.21
# Run go fmt against code
fmt:
@ -153,13 +153,13 @@ fuzz-native:
./tests/fuzz/native_go_run.sh
# Find or download controller-gen
CONTROLLER_GEN = $(GOBIN)/controller-gen
CONTROLLER_GEN = $(shell pwd)/bin/controller-gen
.PHONY: controller-gen
controller-gen: ## Download controller-gen locally if necessary.
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_GEN_VERSION))
# Find or download gen-crd-api-reference-docs
GEN_CRD_API_REFERENCE_DOCS = $(GOBIN)/gen-crd-api-reference-docs
GEN_CRD_API_REFERENCE_DOCS = $(shell pwd)/bin/gen-crd-api-reference-docs
.PHONY: gen-crd-api-reference-docs
gen-crd-api-reference-docs:
$(call go-install-tool,$(GEN_CRD_API_REFERENCE_DOCS),github.com/ahmetb/gen-crd-api-reference-docs@$(GEN_API_REF_DOCS_VERSION))
@ -171,7 +171,7 @@ install-envtest: setup-envtest
mkdir -p ${ENVTEST_ASSETS_DIR}
$(ENVTEST) use $(ENVTEST_KUBERNETES_VERSION) --arch=$(ENVTEST_ARCH) --bin-dir=$(ENVTEST_ASSETS_DIR)
ENVTEST = $(GOBIN)/setup-envtest
ENVTEST = $(shell pwd)/bin/setup-envtest
.PHONY: envtest
setup-envtest: ## Download envtest-setup locally if necessary.
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest)
@ -185,7 +185,7 @@ TMP_DIR=$$(mktemp -d) ;\
cd $$TMP_DIR ;\
go mod init tmp ;\
echo "Downloading $(2)" ;\
GOBIN=$(GOBIN) go install $(2) ;\
GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\
rm -rf $$TMP_DIR ;\
}
endef

View File

@ -7,7 +7,7 @@
[![release](https://img.shields.io/github/release/fluxcd/notification-controller/all.svg)](https://github.com/fluxcd/notification-controller/releases)
Event forwarder and notification dispatcher for the [GitOps Toolkit](https://fluxcd.io/flux/components/) controllers.
The notification-controller is an implementation of the [notification.toolkit.fluxcd.io](docs/spec/v1beta3/README.md)
The notification-controller is an implementation of the [notification.toolkit.fluxcd.io](docs/spec/v1beta1/README.md)
API based on the specifications described in the [RFC](docs/spec/README.md).
![overview](docs/diagrams/notification-controller-overview.png)

View File

@ -1,35 +1,31 @@
module github.com/fluxcd/notification-controller/api
go 1.24.0
go 1.20
require (
github.com/fluxcd/pkg/apis/meta v1.18.0
k8s.io/apimachinery v0.33.2
sigs.k8s.io/controller-runtime v0.21.0
github.com/fluxcd/pkg/apis/meta v1.2.0
k8s.io/apimachinery v0.28.4
sigs.k8s.io/controller-runtime v0.16.3
)
// Fix CVE-2022-28948
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
require (
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
sigs.k8s.io/yaml v1.5.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
)

View File

@ -2,29 +2,25 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fluxcd/pkg/apis/meta v1.18.0 h1:ACHrMIjlcioE9GKS7NGk62KX4NshqNewr8sBwMcXABs=
github.com/fluxcd/pkg/apis/meta v1.18.0/go.mod h1:97l3hTwBpJbXBY+wetNbqrUsvES8B1jGioKcBUxmqd8=
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-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/fluxcd/pkg/apis/meta v1.2.0 h1:O766PzGAdMdQKybSflGL8oV0+GgCNIkdsxfalRyzeO8=
github.com/fluxcd/pkg/apis/meta v1.2.0/go.mod h1:fU/Az9AoVyIxC0oI4ihG0NVMNnvrcCzdEym3wxjIQsc=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
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/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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=
@ -34,28 +30,17 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
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=
@ -65,26 +50,24 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/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/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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
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.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
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=
@ -94,25 +77,21 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v2 v2.2.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=
sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM=
k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8=
k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg=
k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0=
k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo=
k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI=
k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4=
sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0=
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=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=

View File

@ -40,14 +40,13 @@ const (
GCRReceiver string = "gcr"
NexusReceiver string = "nexus"
ACRReceiver string = "acr"
CDEventsReceiver string = "cdevents"
)
// ReceiverSpec defines the desired state of the Receiver.
type ReceiverSpec struct {
// Type of webhook sender, used to determine
// the validation procedure and payload deserialization.
// +kubebuilder:validation:Enum=generic;generic-hmac;github;gitlab;bitbucket;harbor;dockerhub;quay;gcr;nexus;acr;cdevents
// +kubebuilder:validation:Enum=generic;generic-hmac;github;gitlab;bitbucket;harbor;dockerhub;quay;gcr;nexus;acr
// +required
Type string `json:"type"`
@ -67,16 +66,6 @@ type ReceiverSpec struct {
// +required
Resources []CrossNamespaceObjectReference `json:"resources"`
// ResourceFilter is a CEL expression expected to return a boolean that is
// evaluated for each resource referenced in the Resources field when a
// webhook is received. If the expression returns false then the controller
// will not request a reconciliation for the resource.
// When the expression is specified the controller will parse it and mark
// the object as terminally failed if the expression is invalid or does not
// return a boolean.
// +optional
ResourceFilter string `json:"resourceFilter,omitempty"`
// SecretRef specifies the Secret containing the token used
// to validate the payload authenticity.
// +required
@ -133,6 +122,7 @@ func (in *Receiver) GetInterval() time.Duration {
}
// +genclient
// +genclient:Namespaced
// +kubebuilder:storageversion
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

View File

@ -31,13 +31,13 @@ type CrossNamespaceObjectReference struct {
// Name of the referent
// If multiple resources are targeted `*` may be set.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:MaxLength=53
// +required
Name string `json:"name"`
// Namespace of the referent
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:MaxLength=53
// +kubebuilder:validation:Optional
// +optional
Namespace string `json:"namespace,omitempty"`

View File

@ -1,4 +1,5 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 2023 The Flux authors

View File

@ -67,8 +67,13 @@ type AlertStatus struct {
}
// +genclient
// +genclient:Namespaced
// +kubebuilder:object:root=true
// +kubebuilder:skipversion
// +kubebuilder:subresource:status
// +kubebuilder:deprecatedversion:warning="v1beta1 Alert is deprecated, upgrade to v1beta3"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description=""
// Alert is the Schema for the alerts API
type Alert struct {

View File

@ -15,9 +15,6 @@ limitations under the License.
*/
// Package v1beta1 contains API Schema definitions for the notification v1beta1 API group
//
// Deprecated: v1beta1 is no longer supported, use v1 and v1beta3 instead.
//
// +kubebuilder:object:generate=true
// +groupName=notification.toolkit.fluxcd.io
package v1beta1

View File

@ -110,8 +110,13 @@ type ProviderStatus struct {
}
// +genclient
// +genclient:Namespaced
// +kubebuilder:object:root=true
// +kubebuilder:skipversion
// +kubebuilder:subresource:status
// +kubebuilder:deprecatedversion:warning="v1beta1 Provider is deprecated, upgrade to v1beta3"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description=""
// Provider is the Schema for the providers API
type Provider struct {

View File

@ -97,8 +97,13 @@ func (in *Receiver) SetConditions(conditions []metav1.Condition) {
}
// +genclient
// +genclient:Namespaced
// +kubebuilder:object:root=true
// +kubebuilder:skipversion
// +kubebuilder:subresource:status
// +kubebuilder:deprecatedversion:warning="v1beta1 Receiver is deprecated, upgrade to v1"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description=""
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description=""
// Receiver is the Schema for the receivers API
type Receiver struct {

View File

@ -1,4 +1,5 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 2023 The Flux authors

View File

@ -88,6 +88,7 @@ type AlertStatus struct {
}
// +genclient
// +genclient:Namespaced
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:deprecatedversion:warning="v1beta2 Alert is deprecated, upgrade to v1beta3"

View File

@ -131,6 +131,7 @@ type ProviderStatus struct {
}
// +genclient
// +genclient:Namespaced
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:deprecatedversion:warning="v1beta2 Provider is deprecated, upgrade to v1beta3"

View File

@ -129,6 +129,7 @@ func (in *Receiver) GetInterval() time.Duration {
}
// +genclient
// +genclient:Namespaced
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:deprecatedversion:warning="v1beta2 Receiver is deprecated, upgrade to v1"

View File

@ -1,4 +1,5 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 2023 The Flux authors

View File

@ -64,11 +64,8 @@ type AlertSpec struct {
ExclusionList []string `json:"exclusionList,omitempty"`
// Summary holds a short description of the impact and affected cluster.
// Deprecated: Use EventMetadata instead.
//
// +kubebuilder:validation:MaxLength:=255
// +optional
// +deprecated
Summary string `json:"summary,omitempty"`
// Suspend tells the controller to suspend subsequent
@ -78,6 +75,7 @@ type AlertSpec struct {
}
// +genclient
// +genclient:Namespaced
// +kubebuilder:storageversion
// +kubebuilder:object:root=true
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""

View File

@ -55,22 +55,12 @@ const (
)
// ProviderSpec defines the desired state of the Provider.
// +kubebuilder:validation:XValidation:rule="self.type == 'github' || self.type == 'gitlab' || self.type == 'gitea' || self.type == 'bitbucketserver' || self.type == 'bitbucket' || self.type == 'azuredevops' || !has(self.commitStatusExpr)", message="spec.commitStatusExpr is only supported for the 'github', 'gitlab', 'gitea', 'bitbucketserver', 'bitbucket', 'azuredevops' provider types"
type ProviderSpec struct {
// Type specifies which Provider implementation to use.
// +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;generic-hmac;github;gitlab;gitea;bitbucketserver;bitbucket;azuredevops;googlechat;googlepubsub;webex;sentry;azureeventhub;telegram;lark;matrix;opsgenie;alertmanager;grafana;githubdispatch;pagerduty;datadog;nats
// +required
Type string `json:"type"`
// Interval at which to reconcile the Provider with its Secret references.
// Deprecated and not used in v1beta3.
//
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:Pattern="^([0-9]+(\\.[0-9]+)?(ms|s|m|h))+$"
// +optional
// +deprecated
Interval *metav1.Duration `json:"interval,omitempty"`
// Channel specifies the destination channel where events should be posted.
// +kubebuilder:validation:MaxLength:=2048
// +optional
@ -97,72 +87,33 @@ type ProviderSpec struct {
Timeout *metav1.Duration `json:"timeout,omitempty"`
// Proxy the HTTP/S address of the proxy server.
// Deprecated: Use ProxySecretRef instead. Will be removed in v1.
// +kubebuilder:validation:Pattern="^(http|https)://.*$"
// +kubebuilder:validation:MaxLength:=2048
// +kubebuilder:validation:Optional
// +optional
Proxy string `json:"proxy,omitempty"`
// ProxySecretRef specifies the Secret containing the proxy configuration
// for this Provider. The Secret should contain an 'address' key with the
// HTTP/S address of the proxy server. Optional 'username' and 'password'
// keys can be provided for proxy authentication.
// +optional
ProxySecretRef *meta.LocalObjectReference `json:"proxySecretRef,omitempty"`
// SecretRef specifies the Secret containing the authentication
// credentials for this Provider.
// +optional
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
// ServiceAccountName is the name of the Kubernetes ServiceAccount used to
// authenticate with cloud provider services through workload identity.
// This enables multi-tenant authentication without storing static credentials.
//
// Supported provider types: azureeventhub, azuredevops, googlepubsub
//
// When specified, the controller will:
// 1. Create an OIDC token for the specified ServiceAccount
// 2. Exchange it for cloud provider credentials via STS
// 3. Use the obtained credentials for API authentication
//
// When unspecified, controller-level authentication is used (single-tenant).
//
// An error is thrown if static credentials are also defined in SecretRef.
// This field requires the ObjectLevelWorkloadIdentity feature gate to be enabled.
//
// CertSecretRef specifies the Secret containing
// a PEM-encoded CA certificate (in the `ca.crt` key).
// +optional
ServiceAccountName string `json:"serviceAccountName,omitempty"`
// CertSecretRef specifies the Secret containing TLS certificates
// for secure communication.
//
// Supported configurations:
// - CA-only: Server authentication (provide ca.crt only)
// - mTLS: Mutual authentication (provide ca.crt + tls.crt + tls.key)
// - Client-only: Client authentication with system CA (provide tls.crt + tls.key only)
//
// Legacy keys "caFile", "certFile", "keyFile" are supported but deprecated. Use "ca.crt", "tls.crt", "tls.key" instead.
//
// +optional
// Note: Support for the `caFile` key has
// been deprecated.
CertSecretRef *meta.LocalObjectReference `json:"certSecretRef,omitempty"`
// Suspend tells the controller to suspend subsequent
// events handling for this Provider.
// +optional
Suspend bool `json:"suspend,omitempty"`
// CommitStatusExpr is a CEL expression that evaluates to a string value
// that can be used to generate a custom commit status message for use
// with eligible Provider types (github, gitlab, gitea, bitbucketserver,
// bitbucket, azuredevops). Supported variables are: event, provider,
// and alert.
// +optional
CommitStatusExpr string `json:"commitStatusExpr,omitempty"`
}
// +genclient
// +genclient:Namespaced
// +kubebuilder:storageversion
// +kubebuilder:object:root=true
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""

View File

@ -1,4 +1,5 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright 2023 The Flux authors
@ -186,21 +187,11 @@ func (in *ProviderList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProviderSpec) DeepCopyInto(out *ProviderSpec) {
*out = *in
if in.Interval != nil {
in, out := &in.Interval, &out.Interval
*out = new(metav1.Duration)
**out = **in
}
if in.Timeout != nil {
in, out := &in.Timeout, &out.Timeout
*out = new(metav1.Duration)
**out = **in
}
if in.ProxySecretRef != nil {
in, out := &in.ProxySecretRef, &out.ProxySecretRef
*out = new(meta.LocalObjectReference)
**out = **in
}
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(meta.LocalObjectReference)

View File

@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.1
controller-gen.kubebuilder.io/version: v0.12.0
name: alerts.notification.toolkit.fluxcd.io
spec:
group: notification.toolkit.fluxcd.io
@ -25,60 +25,41 @@ spec:
name: Status
type: string
deprecated: true
deprecationWarning: v1beta2 Alert is deprecated, upgrade to v1beta3
name: v1beta2
deprecationWarning: v1beta1 Alert is deprecated, upgrade to v1beta3
name: v1beta1
schema:
openAPIV3Schema:
description: Alert is the Schema for the alerts API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: AlertSpec defines an alerting rule for events involving a
list of objects.
list of objects
properties:
eventMetadata:
additionalProperties:
type: string
description: |-
EventMetadata is an optional field for adding metadata to events dispatched by the
controller. This can be used for enhancing the context of the event. If a field
would override one already present on the original event as generated by the emitter,
then the override doesn't happen, i.e. the original value is preserved, and an info
log is printed.
type: object
eventSeverity:
default: info
description: |-
EventSeverity specifies how to filter events based on severity.
description: Filter events based on severity, defaults to ('info').
If set to 'info' no events will be filtered.
enum:
- info
- error
type: string
eventSources:
description: |-
EventSources specifies how to filter events based
on the involved object kind, name and namespace.
description: Filter events based on the involved objects.
items:
description: |-
CrossNamespaceObjectReference contains enough information to let you locate the
typed referenced object at cluster level
description: CrossNamespaceObjectReference contains enough information
to let you locate the typed referenced object at cluster level
properties:
apiVersion:
description: API version of the referent
@ -100,22 +81,228 @@ spec:
matchLabels:
additionalProperties:
type: string
description: |-
MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
MatchLabels requires the name to be set to `*`.
description: MatchLabels is a map of {key,value} pairs. A single
{key,value} in the matchLabels map is equivalent to an element
of matchExpressions, whose key field is "key", the operator
is "In", and the values array contains only "value". The requirements
are ANDed.
type: object
name:
description: |-
Name of the referent
If multiple resources are targeted `*` may be set.
maxLength: 253
description: Name of the referent
maxLength: 53
minLength: 1
type: string
namespace:
description: Namespace of the referent
maxLength: 253
maxLength: 53
minLength: 1
type: string
required:
- name
type: object
type: array
exclusionList:
description: A list of Golang regular expressions to be used for excluding
messages.
items:
type: string
type: array
providerRef:
description: Send events using this provider.
properties:
name:
description: Name of the referent.
type: string
required:
- name
type: object
summary:
description: Short description of the impact and affected cluster.
type: string
suspend:
description: This flag tells the controller to suspend subsequent
events dispatching. Defaults to false.
type: boolean
required:
- eventSources
- providerRef
type: object
status:
default:
observedGeneration: -1
description: AlertStatus defines the observed state of Alert
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the last observed generation.
format: int64
type: integer
type: object
type: object
served: true
storage: false
subresources:
status: {}
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Status
type: string
deprecated: true
deprecationWarning: v1beta2 Alert is deprecated, upgrade to v1beta3
name: v1beta2
schema:
openAPIV3Schema:
description: Alert is the Schema for the alerts API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: AlertSpec defines an alerting rule for events involving a
list of objects.
properties:
eventMetadata:
additionalProperties:
type: string
description: EventMetadata is an optional field for adding metadata
to events dispatched by the controller. This can be used for enhancing
the context of the event. If a field would override one already
present on the original event as generated by the emitter, then
the override doesn't happen, i.e. the original value is preserved,
and an info log is printed.
type: object
eventSeverity:
default: info
description: EventSeverity specifies how to filter events based on
severity. If set to 'info' no events will be filtered.
enum:
- info
- error
type: string
eventSources:
description: EventSources specifies how to filter events based on
the involved object kind, name and namespace.
items:
description: CrossNamespaceObjectReference contains enough information
to let you locate the typed referenced object at cluster level
properties:
apiVersion:
description: API version of the referent
type: string
kind:
description: Kind of the referent
enum:
- Bucket
- GitRepository
- Kustomization
- HelmRelease
- HelmChart
- HelmRepository
- ImageRepository
- ImagePolicy
- ImageUpdateAutomation
- OCIRepository
type: string
matchLabels:
additionalProperties:
type: string
description: MatchLabels is a map of {key,value} pairs. A single
{key,value} in the matchLabels map is equivalent to an element
of matchExpressions, whose key field is "key", the operator
is "In", and the values array contains only "value". The requirements
are ANDed. MatchLabels requires the name to be set to `*`.
type: object
name:
description: Name of the referent If multiple resources are
targeted `*` may be set.
maxLength: 53
minLength: 1
type: string
namespace:
description: Namespace of the referent
maxLength: 53
minLength: 1
type: string
required:
@ -124,15 +311,13 @@ spec:
type: object
type: array
exclusionList:
description: |-
ExclusionList specifies a list of Golang regular expressions
description: ExclusionList specifies a list of Golang regular expressions
to be used for excluding messages.
items:
type: string
type: array
inclusionList:
description: |-
InclusionList specifies a list of Golang regular expressions
description: InclusionList specifies a list of Golang regular expressions
to be used for including messages.
items:
type: string
@ -153,9 +338,8 @@ spec:
maxLength: 255
type: string
suspend:
description: |-
Suspend tells the controller to suspend subsequent
events handling for this Alert.
description: Suspend tells the controller to suspend subsequent events
handling for this Alert.
type: boolean
required:
- eventSources
@ -169,35 +353,43 @@ spec:
conditions:
description: Conditions holds the conditions for the Alert.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
@ -212,6 +404,10 @@ spec:
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
@ -224,10 +420,9 @@ spec:
type: object
type: array
lastHandledReconcileAt:
description: |-
LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change of the annotation value
can be detected.
description: LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change of the annotation value can
be detected.
type: string
observedGeneration:
description: ObservedGeneration is the last observed generation.
@ -249,19 +444,14 @@ spec:
description: Alert is the Schema for the alerts API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@ -272,30 +462,27 @@ spec:
eventMetadata:
additionalProperties:
type: string
description: |-
EventMetadata is an optional field for adding metadata to events dispatched by the
controller. This can be used for enhancing the context of the event. If a field
would override one already present on the original event as generated by the emitter,
then the override doesn't happen, i.e. the original value is preserved, and an info
log is printed.
description: EventMetadata is an optional field for adding metadata
to events dispatched by the controller. This can be used for enhancing
the context of the event. If a field would override one already
present on the original event as generated by the emitter, then
the override doesn't happen, i.e. the original value is preserved,
and an info log is printed.
type: object
eventSeverity:
default: info
description: |-
EventSeverity specifies how to filter events based on severity.
If set to 'info' no events will be filtered.
description: EventSeverity specifies how to filter events based on
severity. If set to 'info' no events will be filtered.
enum:
- info
- error
type: string
eventSources:
description: |-
EventSources specifies how to filter events based
on the involved object kind, name and namespace.
description: EventSources specifies how to filter events based on
the involved object kind, name and namespace.
items:
description: |-
CrossNamespaceObjectReference contains enough information to let you locate the
typed referenced object at cluster level
description: CrossNamespaceObjectReference contains enough information
to let you locate the typed referenced object at cluster level
properties:
apiVersion:
description: API version of the referent
@ -317,22 +504,21 @@ spec:
matchLabels:
additionalProperties:
type: string
description: |-
MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
MatchLabels requires the name to be set to `*`.
description: MatchLabels is a map of {key,value} pairs. A single
{key,value} in the matchLabels map is equivalent to an element
of matchExpressions, whose key field is "key", the operator
is "In", and the values array contains only "value". The requirements
are ANDed. MatchLabels requires the name to be set to `*`.
type: object
name:
description: |-
Name of the referent
If multiple resources are targeted `*` may be set.
maxLength: 253
description: Name of the referent If multiple resources are
targeted `*` may be set.
maxLength: 53
minLength: 1
type: string
namespace:
description: Namespace of the referent
maxLength: 253
maxLength: 53
minLength: 1
type: string
required:
@ -341,15 +527,13 @@ spec:
type: object
type: array
exclusionList:
description: |-
ExclusionList specifies a list of Golang regular expressions
description: ExclusionList specifies a list of Golang regular expressions
to be used for excluding messages.
items:
type: string
type: array
inclusionList:
description: |-
InclusionList specifies a list of Golang regular expressions
description: InclusionList specifies a list of Golang regular expressions
to be used for including messages.
items:
type: string
@ -365,15 +549,13 @@ spec:
- name
type: object
summary:
description: |-
Summary holds a short description of the impact and affected cluster.
Deprecated: Use EventMetadata instead.
description: Summary holds a short description of the impact and affected
cluster.
maxLength: 255
type: string
suspend:
description: |-
Suspend tells the controller to suspend subsequent
events handling for this Alert.
description: Suspend tells the controller to suspend subsequent events
handling for this Alert.
type: boolean
required:
- eventSources

View File

@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.1
controller-gen.kubebuilder.io/version: v0.12.0
name: providers.notification.toolkit.fluxcd.io
spec:
group: notification.toolkit.fluxcd.io
@ -14,6 +14,191 @@ spec:
singular: provider
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Status
type: string
deprecated: true
deprecationWarning: v1beta1 Provider is deprecated, upgrade to v1beta3
name: v1beta1
schema:
openAPIV3Schema:
description: Provider is the Schema for the providers API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: ProviderSpec defines the desired state of Provider
properties:
address:
description: HTTP/S webhook address of this provider
pattern: ^(http|https)://
type: string
certSecretRef:
description: CertSecretRef can be given the name of a secret containing
a PEM-encoded CA certificate (`caFile`)
properties:
name:
description: Name of the referent.
type: string
required:
- name
type: object
channel:
description: Alert channel for this provider
type: string
proxy:
description: HTTP/S address of the proxy
pattern: ^(http|https)://
type: string
secretRef:
description: Secret reference containing the provider webhook URL
using "address" as data key
properties:
name:
description: Name of the referent.
type: string
required:
- name
type: object
suspend:
description: This flag tells the controller to suspend subsequent
events handling. Defaults to false.
type: boolean
timeout:
description: Timeout for sending alerts to the provider.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$
type: string
type:
description: Type of provider
enum:
- slack
- discord
- msteams
- rocket
- generic
- generic-hmac
- github
- gitlab
- bitbucket
- azuredevops
- googlechat
- webex
- sentry
- azureeventhub
- telegram
- lark
- matrix
- opsgenie
- alertmanager
- grafana
- githubdispatch
type: string
username:
description: Bot username for this provider
type: string
required:
- type
type: object
status:
default:
observedGeneration: -1
description: ProviderStatus defines the observed state of Provider
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the last reconciled generation.
format: int64
type: integer
type: object
type: object
served: true
storage: false
subresources:
status: {}
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
@ -32,19 +217,14 @@ spec:
description: Provider is the Schema for the providers API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@ -52,20 +232,17 @@ spec:
description: ProviderSpec defines the desired state of the Provider.
properties:
address:
description: |-
Address specifies the endpoint, in a generic sense, to where alerts are sent.
What kind of endpoint depends on the specific Provider type being used.
For the generic Provider, for example, this is an HTTP/S address.
For other Provider types this could be a project ID or a namespace.
description: Address specifies the endpoint, in a generic sense, to
where alerts are sent. What kind of endpoint depends on the specific
Provider type being used. For the generic Provider, for example,
this is an HTTP/S address. For other Provider types this could be
a project ID or a namespace.
maxLength: 2048
type: string
certSecretRef:
description: |-
CertSecretRef specifies the Secret containing
a PEM-encoded CA certificate (in the `ca.crt` key).
Note: Support for the `caFile` key has
been deprecated.
description: "CertSecretRef specifies the Secret containing a PEM-encoded
CA certificate (in the `ca.crt` key). \n Note: Support for the `caFile`
key has been deprecated."
properties:
name:
description: Name of the referent.
@ -89,8 +266,7 @@ spec:
pattern: ^(http|https)://.*$
type: string
secretRef:
description: |-
SecretRef specifies the Secret containing the authentication
description: SecretRef specifies the Secret containing the authentication
credentials for this Provider.
properties:
name:
@ -100,9 +276,8 @@ spec:
- name
type: object
suspend:
description: |-
Suspend tells the controller to suspend subsequent
events handling for this Provider.
description: Suspend tells the controller to suspend subsequent events
handling for this Provider.
type: boolean
timeout:
description: Timeout for sending alerts to the Provider.
@ -153,35 +328,43 @@ spec:
conditions:
description: Conditions holds the conditions for the Provider.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
@ -196,6 +379,10 @@ spec:
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
@ -208,10 +395,9 @@ spec:
type: object
type: array
lastHandledReconcileAt:
description: |-
LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change of the annotation value
can be detected.
description: LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change of the annotation value can
be detected.
type: string
observedGeneration:
description: ObservedGeneration is the last reconciled generation.
@ -233,19 +419,14 @@ spec:
description: Provider is the Schema for the providers API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@ -253,24 +434,17 @@ spec:
description: ProviderSpec defines the desired state of the Provider.
properties:
address:
description: |-
Address specifies the endpoint, in a generic sense, to where alerts are sent.
What kind of endpoint depends on the specific Provider type being used.
For the generic Provider, for example, this is an HTTP/S address.
For other Provider types this could be a project ID or a namespace.
description: Address specifies the endpoint, in a generic sense, to
where alerts are sent. What kind of endpoint depends on the specific
Provider type being used. For the generic Provider, for example,
this is an HTTP/S address. For other Provider types this could be
a project ID or a namespace.
maxLength: 2048
type: string
certSecretRef:
description: |-
CertSecretRef specifies the Secret containing TLS certificates
for secure communication.
Supported configurations:
- CA-only: Server authentication (provide ca.crt only)
- mTLS: Mutual authentication (provide ca.crt + tls.crt + tls.key)
- Client-only: Client authentication with system CA (provide tls.crt + tls.key only)
Legacy keys "caFile", "certFile", "keyFile" are supported but deprecated. Use "ca.crt", "tls.crt", "tls.key" instead.
description: "CertSecretRef specifies the Secret containing a PEM-encoded
CA certificate (in the `ca.crt` key). \n Note: Support for the `caFile`
key has been deprecated."
properties:
name:
description: Name of the referent.
@ -283,43 +457,13 @@ spec:
should be posted.
maxLength: 2048
type: string
commitStatusExpr:
description: |-
CommitStatusExpr is a CEL expression that evaluates to a string value
that can be used to generate a custom commit status message for use
with eligible Provider types (github, gitlab, gitea, bitbucketserver,
bitbucket, azuredevops). Supported variables are: event, provider,
and alert.
type: string
interval:
description: |-
Interval at which to reconcile the Provider with its Secret references.
Deprecated and not used in v1beta3.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
proxy:
description: |-
Proxy the HTTP/S address of the proxy server.
Deprecated: Use ProxySecretRef instead. Will be removed in v1.
description: Proxy the HTTP/S address of the proxy server.
maxLength: 2048
pattern: ^(http|https)://.*$
type: string
proxySecretRef:
description: |-
ProxySecretRef specifies the Secret containing the proxy configuration
for this Provider. The Secret should contain an 'address' key with the
HTTP/S address of the proxy server. Optional 'username' and 'password'
keys can be provided for proxy authentication.
properties:
name:
description: Name of the referent.
type: string
required:
- name
type: object
secretRef:
description: |-
SecretRef specifies the Secret containing the authentication
description: SecretRef specifies the Secret containing the authentication
credentials for this Provider.
properties:
name:
@ -328,28 +472,9 @@ spec:
required:
- name
type: object
serviceAccountName:
description: |-
ServiceAccountName is the name of the Kubernetes ServiceAccount used to
authenticate with cloud provider services through workload identity.
This enables multi-tenant authentication without storing static credentials.
Supported provider types: azureeventhub, azuredevops, googlepubsub
When specified, the controller will:
1. Create an OIDC token for the specified ServiceAccount
2. Exchange it for cloud provider credentials via STS
3. Use the obtained credentials for API authentication
When unspecified, controller-level authentication is used (single-tenant).
An error is thrown if static credentials are also defined in SecretRef.
This field requires the ObjectLevelWorkloadIdentity feature gate to be enabled.
type: string
suspend:
description: |-
Suspend tells the controller to suspend subsequent
events handling for this Provider.
description: Suspend tells the controller to suspend subsequent events
handling for this Provider.
type: boolean
timeout:
description: Timeout for sending alerts to the Provider.
@ -393,12 +518,6 @@ spec:
required:
- type
type: object
x-kubernetes-validations:
- message: spec.commitStatusExpr is only supported for the 'github', 'gitlab',
'gitea', 'bitbucketserver', 'bitbucket', 'azuredevops' provider types
rule: self.type == 'github' || self.type == 'gitlab' || self.type ==
'gitea' || self.type == 'bitbucketserver' || self.type == 'bitbucket'
|| self.type == 'azuredevops' || !has(self.commitStatusExpr)
type: object
served: true
storage: true

View File

@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.1
controller-gen.kubebuilder.io/version: v0.12.0
name: receivers.notification.toolkit.fluxcd.io
spec:
group: notification.toolkit.fluxcd.io
@ -30,19 +30,14 @@ spec:
description: Receiver is the Schema for the receivers API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@ -50,9 +45,8 @@ spec:
description: ReceiverSpec defines the desired state of the Receiver.
properties:
events:
description: |-
Events specifies the list of event types to handle,
e.g. 'push' for GitHub or 'Push Hook' for GitLab.
description: Events specifies the list of event types to handle, e.g.
'push' for GitHub or 'Push Hook' for GitLab.
items:
type: string
type: array
@ -62,22 +56,11 @@ spec:
Secret references.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
resourceFilter:
description: |-
ResourceFilter is a CEL expression expected to return a boolean that is
evaluated for each resource referenced in the Resources field when a
webhook is received. If the expression returns false then the controller
will not request a reconciliation for the resource.
When the expression is specified the controller will parse it and mark
the object as terminally failed if the expression is invalid or does not
return a boolean.
type: string
resources:
description: A list of resources to be notified about changes.
items:
description: |-
CrossNamespaceObjectReference contains enough information to let you locate the
typed referenced object at cluster level
description: CrossNamespaceObjectReference contains enough information
to let you locate the typed referenced object at cluster level
properties:
apiVersion:
description: API version of the referent
@ -99,22 +82,21 @@ spec:
matchLabels:
additionalProperties:
type: string
description: |-
MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
MatchLabels requires the name to be set to `*`.
description: MatchLabels is a map of {key,value} pairs. A single
{key,value} in the matchLabels map is equivalent to an element
of matchExpressions, whose key field is "key", the operator
is "In", and the values array contains only "value". The requirements
are ANDed. MatchLabels requires the name to be set to `*`.
type: object
name:
description: |-
Name of the referent
If multiple resources are targeted `*` may be set.
maxLength: 253
description: Name of the referent If multiple resources are
targeted `*` may be set.
maxLength: 53
minLength: 1
type: string
namespace:
description: Namespace of the referent
maxLength: 253
maxLength: 53
minLength: 1
type: string
required:
@ -123,8 +105,7 @@ spec:
type: object
type: array
secretRef:
description: |-
SecretRef specifies the Secret containing the token used
description: SecretRef specifies the Secret containing the token used
to validate the payload authenticity.
properties:
name:
@ -134,14 +115,12 @@ spec:
- name
type: object
suspend:
description: |-
Suspend tells the controller to suspend subsequent
events handling for this receiver.
description: Suspend tells the controller to suspend subsequent events
handling for this receiver.
type: boolean
type:
description: |-
Type of webhook sender, used to determine
the validation procedure and payload deserialization.
description: Type of webhook sender, used to determine the validation
procedure and payload deserialization.
enum:
- generic
- generic-hmac
@ -154,7 +133,6 @@ spec:
- gcr
- nexus
- acr
- cdevents
type: string
required:
- resources
@ -169,35 +147,43 @@ spec:
conditions:
description: Conditions holds the conditions for the Receiver.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
@ -212,6 +198,10 @@ spec:
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
@ -224,10 +214,9 @@ spec:
type: object
type: array
lastHandledReconcileAt:
description: |-
LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change of the annotation value
can be detected.
description: LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change of the annotation value can
be detected.
type: string
observedGeneration:
description: ObservedGeneration is the last observed generation of
@ -235,9 +224,8 @@ spec:
format: int64
type: integer
webhookPath:
description: |-
WebhookPath is the generated incoming webhook address in the format
of '/hook/sha256sum(token+name+namespace)'.
description: WebhookPath is the generated incoming webhook address
in the format of '/hook/sha256sum(token+name+namespace)'.
type: string
type: object
type: object
@ -245,6 +233,210 @@ spec:
storage: true
subresources:
status: {}
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Status
type: string
deprecated: true
deprecationWarning: v1beta1 Receiver is deprecated, upgrade to v1
name: v1beta1
schema:
openAPIV3Schema:
description: Receiver is the Schema for the receivers API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: ReceiverSpec defines the desired state of Receiver
properties:
events:
description: A list of events to handle, e.g. 'push' for GitHub or
'Push Hook' for GitLab.
items:
type: string
type: array
resources:
description: A list of resources to be notified about changes.
items:
description: CrossNamespaceObjectReference contains enough information
to let you locate the typed referenced object at cluster level
properties:
apiVersion:
description: API version of the referent
type: string
kind:
description: Kind of the referent
enum:
- Bucket
- GitRepository
- Kustomization
- HelmRelease
- HelmChart
- HelmRepository
- ImageRepository
- ImagePolicy
- ImageUpdateAutomation
- OCIRepository
type: string
matchLabels:
additionalProperties:
type: string
description: MatchLabels is a map of {key,value} pairs. A single
{key,value} in the matchLabels map is equivalent to an element
of matchExpressions, whose key field is "key", the operator
is "In", and the values array contains only "value". The requirements
are ANDed.
type: object
name:
description: Name of the referent
maxLength: 53
minLength: 1
type: string
namespace:
description: Namespace of the referent
maxLength: 53
minLength: 1
type: string
required:
- name
type: object
type: array
secretRef:
description: Secret reference containing the token used to validate
the payload authenticity
properties:
name:
description: Name of the referent.
type: string
required:
- name
type: object
suspend:
description: This flag tells the controller to suspend subsequent
events handling. Defaults to false.
type: boolean
type:
description: Type of webhook sender, used to determine the validation
procedure and payload deserialization.
enum:
- generic
- generic-hmac
- github
- gitlab
- bitbucket
- harbor
- dockerhub
- quay
- gcr
- nexus
- acr
type: string
required:
- resources
- type
type: object
status:
default:
observedGeneration: -1
description: ReceiverStatus defines the observed state of Receiver
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the last observed generation.
format: int64
type: integer
url:
description: Generated webhook URL in the format of '/hook/sha256sum(token+name+namespace)'.
type: string
type: object
type: object
served: true
storage: false
subresources:
status: {}
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
@ -263,19 +455,14 @@ spec:
description: Receiver is the Schema for the receivers API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
@ -283,9 +470,8 @@ spec:
description: ReceiverSpec defines the desired state of the Receiver.
properties:
events:
description: |-
Events specifies the list of event types to handle,
e.g. 'push' for GitHub or 'Push Hook' for GitLab.
description: Events specifies the list of event types to handle, e.g.
'push' for GitHub or 'Push Hook' for GitLab.
items:
type: string
type: array
@ -297,9 +483,8 @@ spec:
resources:
description: A list of resources to be notified about changes.
items:
description: |-
CrossNamespaceObjectReference contains enough information to let you locate the
typed referenced object at cluster level
description: CrossNamespaceObjectReference contains enough information
to let you locate the typed referenced object at cluster level
properties:
apiVersion:
description: API version of the referent
@ -321,22 +506,21 @@ spec:
matchLabels:
additionalProperties:
type: string
description: |-
MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
MatchLabels requires the name to be set to `*`.
description: MatchLabels is a map of {key,value} pairs. A single
{key,value} in the matchLabels map is equivalent to an element
of matchExpressions, whose key field is "key", the operator
is "In", and the values array contains only "value". The requirements
are ANDed. MatchLabels requires the name to be set to `*`.
type: object
name:
description: |-
Name of the referent
If multiple resources are targeted `*` may be set.
maxLength: 253
description: Name of the referent If multiple resources are
targeted `*` may be set.
maxLength: 53
minLength: 1
type: string
namespace:
description: Namespace of the referent
maxLength: 253
maxLength: 53
minLength: 1
type: string
required:
@ -345,8 +529,7 @@ spec:
type: object
type: array
secretRef:
description: |-
SecretRef specifies the Secret containing the token used
description: SecretRef specifies the Secret containing the token used
to validate the payload authenticity.
properties:
name:
@ -356,14 +539,12 @@ spec:
- name
type: object
suspend:
description: |-
Suspend tells the controller to suspend subsequent
events handling for this receiver.
description: Suspend tells the controller to suspend subsequent events
handling for this receiver.
type: boolean
type:
description: |-
Type of webhook sender, used to determine
the validation procedure and payload deserialization.
description: Type of webhook sender, used to determine the validation
procedure and payload deserialization.
enum:
- generic
- generic-hmac
@ -379,7 +560,6 @@ spec:
type: string
required:
- resources
- secretRef
- type
type: object
status:
@ -390,35 +570,43 @@ spec:
conditions:
description: Conditions holds the conditions for the Receiver.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
@ -433,6 +621,10 @@ spec:
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
@ -445,10 +637,9 @@ spec:
type: object
type: array
lastHandledReconcileAt:
description: |-
LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change of the annotation value
can be detected.
description: LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change of the annotation value can
be detected.
type: string
observedGeneration:
description: ObservedGeneration is the last observed generation of
@ -456,15 +647,13 @@ spec:
format: int64
type: integer
url:
description: |-
URL is the generated incoming webhook address in the format
of '/hook/sha256sum(token+name+namespace)'.
Deprecated: Replaced by WebhookPath.
description: 'URL is the generated incoming webhook address in the
format of ''/hook/sha256sum(token+name+namespace)''. Deprecated:
Replaced by WebhookPath.'
type: string
webhookPath:
description: |-
WebhookPath is the generated incoming webhook address in the format
of '/hook/sha256sum(token+name+namespace)'.
description: WebhookPath is the generated incoming webhook address
in the format of '/hook/sha256sum(token+name+namespace)'.
type: string
type: object
type: object

View File

@ -6,4 +6,4 @@ resources:
images:
- name: fluxcd/notification-controller
newName: fluxcd/notification-controller
newTag: v1.6.0
newTag: v1.2.2

View File

@ -19,12 +19,6 @@ rules:
- get
- list
- watch
- apiGroups:
- ""
resources:
- serviceaccounts/token
verbs:
- create
- apiGroups:
- image.fluxcd.io
resources:
@ -45,7 +39,29 @@ rules:
- notification.toolkit.fluxcd.io
resources:
- alerts
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- notification.toolkit.fluxcd.io
resources:
- providers
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- notification.toolkit.fluxcd.io
resources:
- receivers
verbs:
- create
@ -67,9 +83,6 @@ rules:
- source.fluxcd.io
resources:
- buckets
- gitrepositories
- helmrepositories
- ocirepositories
verbs:
- get
- list
@ -80,8 +93,53 @@ rules:
- source.fluxcd.io
resources:
- buckets/status
verbs:
- get
- apiGroups:
- source.fluxcd.io
resources:
- gitrepositories
verbs:
- get
- list
- patch
- update
- watch
- apiGroups:
- source.fluxcd.io
resources:
- gitrepositories/status
verbs:
- get
- apiGroups:
- source.fluxcd.io
resources:
- helmrepositories
verbs:
- get
- list
- patch
- update
- watch
- apiGroups:
- source.fluxcd.io
resources:
- helmrepositories/status
verbs:
- get
- apiGroups:
- source.fluxcd.io
resources:
- ocirepositories
verbs:
- get
- list
- patch
- update
- watch
- apiGroups:
- source.fluxcd.io
resources:
- ocirepositories/status
verbs:
- get

8
config/testdata/provider.yaml vendored Normal file
View File

@ -0,0 +1,8 @@
---
apiVersion: notification.toolkit.fluxcd.io/v1beta1
kind: Provider
metadata:
name: status-defaults
spec:
type: generic

View File

@ -122,24 +122,6 @@ e.g. &lsquo;push&rsquo; for GitHub or &lsquo;Push Hook&rsquo; for GitLab.</p>
</tr>
<tr>
<td>
<code>resourceFilter</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ResourceFilter is a CEL expression expected to return a boolean that is
evaluated for each resource referenced in the Resources field when a
webhook is received. If the expression returns false then the controller
will not request a reconciliation for the resource.
When the expression is specified the controller will parse it and mark
the object as terminally failed if the expression is invalid or does not
return a boolean.</p>
</td>
</tr>
<tr>
<td>
<code>secretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
@ -339,24 +321,6 @@ e.g. &lsquo;push&rsquo; for GitHub or &lsquo;Push Hook&rsquo; for GitLab.</p>
</tr>
<tr>
<td>
<code>resourceFilter</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ResourceFilter is a CEL expression expected to return a boolean that is
evaluated for each resource referenced in the Resources field when a
webhook is received. If the expression returns false then the controller
will not request a reconciliation for the resource.
When the expression is specified the controller will parse it and mark
the object as terminally failed if the expression is invalid or does not
return a boolean.</p>
</td>
</tr>
<tr>
<td>
<code>secretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">

View File

@ -161,8 +161,7 @@ string
</td>
<td>
<em>(Optional)</em>
<p>Summary holds a short description of the impact and affected cluster.
Deprecated: Use EventMetadata instead.</p>
<p>Summary holds a short description of the impact and affected cluster.</p>
</td>
</tr>
<tr>
@ -255,21 +254,6 @@ string
</tr>
<tr>
<td>
<code>interval</code><br>
<em>
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
Kubernetes meta/v1.Duration
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Interval at which to reconcile the Provider with its Secret references.
Deprecated and not used in v1beta3.</p>
</td>
</tr>
<tr>
<td>
<code>channel</code><br>
<em>
string
@ -330,25 +314,7 @@ string
</td>
<td>
<em>(Optional)</em>
<p>Proxy the HTTP/S address of the proxy server.
Deprecated: Use ProxySecretRef instead. Will be removed in v1.</p>
</td>
</tr>
<tr>
<td>
<code>proxySecretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>ProxySecretRef specifies the Secret containing the proxy configuration
for this Provider. The Secret should contain an &lsquo;address&rsquo; key with the
HTTP/S address of the proxy server. Optional &lsquo;username&rsquo; and &lsquo;password&rsquo;
keys can be provided for proxy authentication.</p>
<p>Proxy the HTTP/S address of the proxy server.</p>
</td>
</tr>
<tr>
@ -368,28 +334,6 @@ credentials for this Provider.</p>
</tr>
<tr>
<td>
<code>serviceAccountName</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to
authenticate with cloud provider services through workload identity.
This enables multi-tenant authentication without storing static credentials.</p>
<p>Supported provider types: azureeventhub, azuredevops, googlepubsub</p>
<p>When specified, the controller will:
1. Create an OIDC token for the specified ServiceAccount
2. Exchange it for cloud provider credentials via STS
3. Use the obtained credentials for API authentication</p>
<p>When unspecified, controller-level authentication is used (single-tenant).</p>
<p>An error is thrown if static credentials are also defined in SecretRef.
This field requires the ObjectLevelWorkloadIdentity feature gate to be enabled.</p>
</td>
</tr>
<tr>
<td>
<code>certSecretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
@ -399,13 +343,10 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</td>
<td>
<em>(Optional)</em>
<p>CertSecretRef specifies the Secret containing TLS certificates
for secure communication.</p>
<p>Supported configurations:
- CA-only: Server authentication (provide ca.crt only)
- mTLS: Mutual authentication (provide ca.crt + tls.crt + tls.key)
- Client-only: Client authentication with system CA (provide tls.crt + tls.key only)</p>
<p>Legacy keys &ldquo;caFile&rdquo;, &ldquo;certFile&rdquo;, &ldquo;keyFile&rdquo; are supported but deprecated. Use &ldquo;ca.crt&rdquo;, &ldquo;tls.crt&rdquo;, &ldquo;tls.key&rdquo; instead.</p>
<p>CertSecretRef specifies the Secret containing
a PEM-encoded CA certificate (in the <code>ca.crt</code> key).</p>
<p>Note: Support for the <code>caFile</code> key has
been deprecated.</p>
</td>
</tr>
<tr>
@ -421,22 +362,6 @@ bool
events handling for this Provider.</p>
</td>
</tr>
<tr>
<td>
<code>commitStatusExpr</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>CommitStatusExpr is a CEL expression that evaluates to a string value
that can be used to generate a custom commit status message for use
with eligible Provider types (github, gitlab, gitea, bitbucketserver,
bitbucket, azuredevops). Supported variables are: event, provider,
and alert.</p>
</td>
</tr>
</table>
</td>
</tr>
@ -552,8 +477,7 @@ string
</td>
<td>
<em>(Optional)</em>
<p>Summary holds a short description of the impact and affected cluster.
Deprecated: Use EventMetadata instead.</p>
<p>Summary holds a short description of the impact and affected cluster.</p>
</td>
</tr>
<tr>
@ -603,21 +527,6 @@ string
</tr>
<tr>
<td>
<code>interval</code><br>
<em>
<a href="https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration">
Kubernetes meta/v1.Duration
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Interval at which to reconcile the Provider with its Secret references.
Deprecated and not used in v1beta3.</p>
</td>
</tr>
<tr>
<td>
<code>channel</code><br>
<em>
string
@ -678,25 +587,7 @@ string
</td>
<td>
<em>(Optional)</em>
<p>Proxy the HTTP/S address of the proxy server.
Deprecated: Use ProxySecretRef instead. Will be removed in v1.</p>
</td>
</tr>
<tr>
<td>
<code>proxySecretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>ProxySecretRef specifies the Secret containing the proxy configuration
for this Provider. The Secret should contain an &lsquo;address&rsquo; key with the
HTTP/S address of the proxy server. Optional &lsquo;username&rsquo; and &lsquo;password&rsquo;
keys can be provided for proxy authentication.</p>
<p>Proxy the HTTP/S address of the proxy server.</p>
</td>
</tr>
<tr>
@ -716,28 +607,6 @@ credentials for this Provider.</p>
</tr>
<tr>
<td>
<code>serviceAccountName</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to
authenticate with cloud provider services through workload identity.
This enables multi-tenant authentication without storing static credentials.</p>
<p>Supported provider types: azureeventhub, azuredevops, googlepubsub</p>
<p>When specified, the controller will:
1. Create an OIDC token for the specified ServiceAccount
2. Exchange it for cloud provider credentials via STS
3. Use the obtained credentials for API authentication</p>
<p>When unspecified, controller-level authentication is used (single-tenant).</p>
<p>An error is thrown if static credentials are also defined in SecretRef.
This field requires the ObjectLevelWorkloadIdentity feature gate to be enabled.</p>
</td>
</tr>
<tr>
<td>
<code>certSecretRef</code><br>
<em>
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
@ -747,13 +616,10 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
</td>
<td>
<em>(Optional)</em>
<p>CertSecretRef specifies the Secret containing TLS certificates
for secure communication.</p>
<p>Supported configurations:
- CA-only: Server authentication (provide ca.crt only)
- mTLS: Mutual authentication (provide ca.crt + tls.crt + tls.key)
- Client-only: Client authentication with system CA (provide tls.crt + tls.key only)</p>
<p>Legacy keys &ldquo;caFile&rdquo;, &ldquo;certFile&rdquo;, &ldquo;keyFile&rdquo; are supported but deprecated. Use &ldquo;ca.crt&rdquo;, &ldquo;tls.crt&rdquo;, &ldquo;tls.key&rdquo; instead.</p>
<p>CertSecretRef specifies the Secret containing
a PEM-encoded CA certificate (in the <code>ca.crt</code> key).</p>
<p>Note: Support for the <code>caFile</code> key has
been deprecated.</p>
</td>
</tr>
<tr>
@ -769,22 +635,6 @@ bool
events handling for this Provider.</p>
</td>
</tr>
<tr>
<td>
<code>commitStatusExpr</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>CommitStatusExpr is a CEL expression that evaluates to a string value
that can be used to generate a custom commit status message for use
with eligible Provider types (github, gitlab, gitea, bitbucketserver,
bitbucket, azuredevops). Supported variables are: event, provider,
and alert.</p>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -134,7 +134,6 @@ handle the incoming webhook request.
| [Nexus](#nexus) | `nexus` | ❌ |
| [Azure Container Registry](#acr) | `acr` | ❌ |
| [Google Container Registry](#gcr) | `gcr` | ❌ |
| [CDEvents](#cdevents) | `cdevents` | ✅ |
#### Generic
@ -614,35 +613,6 @@ spec:
name: webapp
```
#### CDEvents
When a Receiver's `.spec.type` is set to `cdevents`, the controller will respond to
a [CDEvent Event Payload](https://cdevents.dev/docs/). It will verify the CDEvent
using the [CDEvent Go-SDK](https://github.com/cdevents/sdk-go).
This type of receiver supports filtering using [Events](#events) by comparing the
`type` header to the list of events.
##### CDEvents example
```yaml
---
apiVersion: notification.toolkit.fluxcd.io/v1
kind: Receiver
metadata:
name: cdevents-receiver
namespace: flux-system
spec:
type: cdevents
events:
- "dev.cdevents.change.merged"
secretRef:
name: receiver-token
resources:
- kind: GitRepository
name: webapp
```
### Events
`.spec.events` is an optional field to specify a list of webhook payload event
@ -700,75 +670,6 @@ resources:
**Note:** Cross-namespace references [can be disabled for security
reasons](#disabling-cross-namespace-selectors).
#### Filtering reconciled objects with CEL
To filter the resources that are reconciled you can use [Common Expression Language (CEL)](https://cel.dev/).
For example, to trigger `ImageRepositories` on notifications from [Google Artifact Registry](https://cloud.google.com/artifact-registry/docs/configure-notifications#examples) you can define the following receiver:
```yaml
apiVersion: notification.toolkit.fluxcd.io/v1
kind: Receiver
metadata:
name: gar-receiver
namespace: apps
spec:
type: gcr
secretRef:
name: flux-gar-token
resources:
- apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
name: "*"
matchLabels:
registry: gar
```
This will trigger the reconciliation of all `ImageRepositories` with the label `registry: gar`.
But if you want to only notify `ImageRepository` resources that are referenced from the incoming hook you can use CEL to filter the resources.
```yaml
apiVersion: notification.toolkit.fluxcd.io/v1
kind: Receiver
metadata:
name: gar-receiver
namespace: apps
spec:
type: gcr
secretRef:
name: flux-gar-token
resources:
- apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
name: "*"
matchLabels:
registry: gar
resourceFilter: 'req.tag.contains(res.metadata.name)'
```
If the body of the incoming hook looks like this:
```json
{
"action":"INSERT",
"digest":"us-east1-docker.pkg.dev/my-project/my-repo/hello-world@sha256:6ec128e26cd5...",
"tag":"us-east1-docker.pkg.dev/my-project/my-repo/hello-world:1.1"
}
```
This simple example would match `ImageRepositories` containing the name `hello-world`.
If you want to do more complex processing:
```yaml
resourceFilter: has(res.metadata.annotations) && req.tag.split('/').last().value().split(":").first().value() == res.metadata.annotations['update-image']
```
This would look for an annotation "update-image" on the resource, and match it to the `hello-world` part of the tag name.
**Note:** Currently the `resource` value in the CEL expression only provides the object metadata, this means you can access things like `res.metadata.labels`, `res.metadata.annotations` and `res.metadata.name`.
### Secret reference
`.spec.secretRef.name` is a required field to specify a name reference to a
@ -779,7 +680,7 @@ This token is used to salt the generated [webhook path](#webhook-path), and
depending on the Receiver [type](#supported-receiver-types), to verify the
authenticity of a request.
Example:
#### Secret example
```yaml
---
@ -793,21 +694,6 @@ stringData:
token: <random token>
```
To trigger a reconciliation of the Receiver when changes occur in
the referenced Secret, you can set the following label on the
Secret:
```yaml
metadata:
labels:
reconcile.fluxcd.io/watch: Enabled
```
An alternative to labeling every Secret is setting the
`--watch-configs-label-selector=owner!=helm` flag in
notification-controller, which allows watching all
Secrets except for Helm storage Secrets.
### Interval
`.spec.interval` is an optional field with a default of ten minutes that specifies
@ -827,7 +713,7 @@ When the field is set to `false` or removed, it will resume.
On multi-tenant clusters, platform admins can disable cross-namespace
references with the `--no-cross-namespace-refs=true` flag. When this flag is
set, Receivers can only refer to [Resources](#resources) in the same namespace
as the Receiver object, preventing tenants from triggering
as the [Alert](alerts.md) object, preventing tenants from triggering
reconciliations to another tenant's resources.
### Public Ingress considerations

View File

@ -368,8 +368,7 @@ and use `https://api.telegram.org/` as the api url.
--from-literal=address=https://api.telegram.org
```
Also note that `spec.channel` can be a unique identifier for the target chat,
a unique identifier with the topic identifier for the forum chat
Also note that `spec.channel` can be a unique identifier for the target chat
or username of the target channel (in the format @channelusername)
```yaml
@ -380,7 +379,7 @@ metadata:
namespace: flux-system
spec:
type: telegram
channel: "@fluxtest" # or "-1557265138" (channel id) or "-1552289257:1" (forum chat id with topic id)
channel: "@fluxtest" # or "-1557265138" (channel id)
secretRef:
name: telegram-token
```

View File

@ -447,7 +447,7 @@ metadata:
stringData:
token: <DataDog API Key>
---
apiVersion: notification.toolkit.fluxcd.io/v1beta2
apiVersion: notification.toolkit.fluxcd.io/v1beta1
kind: Alert
metadata:
name: datadog-info
@ -563,9 +563,8 @@ The Event will be formatted into a message string, with the metadata attached
as a list of key-value pairs.
The Provider's [Channel](#channel) is used to set the receiver of the message.
This can be a unique identifier (`-1234567890`) for the target chat,
a unique identifier with the topic identifier (`-1234567890:1`) for the forum chat,
or the username (`@username`) of the target channel.
This can be a unique identifier (`-1234567890`) for the target chat, or
the username (`@username`) of the target channel.
This Provider type does not support the configuration of a [proxy URL](#https-proxy)
or [TLS certificates](#tls-certificates).
@ -587,7 +586,7 @@ metadata:
spec:
type: telegram
address: https://api.telegram.org
channel: "@fluxcd" # or "-1557265138" (channel id) or "-1552289257:1" (forum chat id with topic id)
channel: "@fluxcd" # or "-1557265138" (channel id)
secretRef:
name: telegram-token
```
@ -1540,8 +1539,6 @@ kubectl create secret generic bb-server-token --from-literal=token=<token>
The HTTP access token must have `Repositories (Read/Write)` permission for
the repository specified in `.spec.address`.
**NOTE:** Please provide HTTPS clone URL in the `address` field of this provider. SSH URLs are not supported by this provider type.
#### Azure DevOps
When `.spec.type` is set to `azuredevops`, the referenced secret must contain a key called `token` with the value set to a

View File

@ -28,13 +28,9 @@ metadata:
name: slack
namespace: flux-system
spec:
summary: "Cluster addons impacted in us-east-2"
providerRef:
name: slack-bot
eventMetadata:
summary: Cluster addons impacted
env: prod
cluster: my-cluster
region: us-east-2
eventSeverity: error
eventSources:
- kind: GitRepository
@ -55,7 +51,7 @@ In the above example:
all GitRepositories and Kustomizations in the `flux-system` namespace.
- When an event with severity `error` is received, the controller posts
a message on Slack channel from `.spec.channel`,
containing the metadata and the reconciliation error.
containing the `summary` text and the reconciliation error.
You can run this example by saving the manifests into `slack-alerts.yaml`.
@ -82,15 +78,10 @@ An Alert also needs a
### Summary
`.spec.summary` is an optional field to specify a short description of the impact.
`.spec.summary` is an optional field to specify a short description of the
impact and affected cluster.
The summary max length can't be greater than 255 characters.
**Warning:** Support for `.spec.summary` has been deprecated and will be removed in
Alert API v1 GA. If you have any Alerts using this field, the controller will log a
deprecation warning. Please use [`.spec.eventMetadata.summary`](#event-metadata) or
[object annotations](#event-metadata-from-object-annotations) for defining alert
summary instead.
The summary max length can't be greater than 255 characters.
### Provider reference
@ -155,11 +146,10 @@ preventing tenants from subscribing to another tenant's events.
### Event metadata
`.spec.eventMetadata` is an optional field for adding metadata to events dispatched by
the controller. This can be used for enhancing the context of the event, e.g. with
cluster-level information.
For all the event metadata sources and their precedence order, please refer to
[Event metadata from object annotations](#event-metadata-from-object-annotations).
the controller. This can be used for enhancing the context of the event. If a field
would override one already present on the original event as generated by the emitter,
then the override doesn't happen, i.e. the original value is preserved, and an info
log is printed.
#### Example
@ -178,68 +168,9 @@ spec:
inclusionList:
- ".*succeeded.*"
eventMetadata:
env: production
cluster: my-cluster
region: us-east-1
```
### Event metadata from object annotations
Event metadata has four sources. They are listed below in order of precedence,
from lowest to highest:
1. User-defined metadata on Flux objects, set with the `event.toolkit.fluxcd.io/`
prefix in the keys of the object's `.metadata.annotations`.
2. User-defined metadata on the Alert object, set with [`.spec.eventMetadata`](#event-metadata).
3. User-defined summary on the Alert object, set with [`.spec.summary`](#summary) (deprecated, see docs).
4. Controller-defined metadata, set with the `<controller group>.toolkit.fluxcd.io/`
prefix in the metadata keys of the event payload.
If there are any metadata key conflicts between the sources, the higher
precedence source will override the lower precedence source, and a warning
log and Kubernetes event will be emitted.
#### Example
```yaml
---
apiVersion: notification.toolkit.fluxcd.io/v1beta3
kind: Alert
metadata:
name: <name>
spec:
eventSources:
- kind: HelmRelease
name: '*'
eventMetadata:
env: production
cluster: my-cluster
region: us-east-1
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: my-webapp
annotations:
event.toolkit.fluxcd.io/summary: "my-webapp impacted. Playbook: <URL to playbook>"
event.toolkit.fluxcd.io/deploymentID: e076e315-5a48-41c3-81c8-8d8bdee7d74d
spec:
... # fields omitted for brevity
```
In the above example, the event payload dispatched by the controller will look like this
(most fields omitted for highlighting the metadata):
```json
{
"metadata": {
"env": "production",
"cluster": "my-cluster",
"region": "us-east-1",
"summary": "my-webapp impacted. Playbook: <URL to playbook>",
"deploymentID": "e076e315-5a48-41c3-81c8-8d8bdee7d74d"
}
}
app.kubernetes.io/env: "production"
app.kubernetes.io/cluster: "my-cluster"
app.kubernetes.io/region: "us-east-1"
```
### Event severity

File diff suppressed because it is too large Load Diff

280
go.mod
View File

@ -1,205 +1,173 @@
module github.com/fluxcd/notification-controller
go 1.24.0
go 1.21
replace github.com/fluxcd/notification-controller/api => ./api
require (
cloud.google.com/go/pubsub v1.49.0
code.gitea.io/sdk/gitea v0.21.0
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1
github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs/v2 v2.0.0
github.com/DataDog/datadog-api-client-go/v2 v2.43.0
github.com/PagerDuty/go-pagerduty v1.8.0
github.com/cdevents/sdk-go v0.4.1
cloud.google.com/go/pubsub v1.33.0
code.gitea.io/sdk/gitea v0.17.0
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
github.com/Azure/azure-amqp-common-go/v4 v4.2.0
github.com/Azure/azure-event-hubs-go/v3 v3.6.1
github.com/DataDog/datadog-api-client-go/v2 v2.19.0
github.com/PagerDuty/go-pagerduty v1.7.0
github.com/chainguard-dev/git-urls v1.0.2
github.com/elazarl/goproxy v1.7.2
github.com/fluxcd/cli-utils v0.36.0-flux.14
github.com/fluxcd/notification-controller/api v1.6.0
github.com/fluxcd/pkg/apis/event v0.18.0
github.com/fluxcd/pkg/apis/meta v1.18.0
github.com/fluxcd/pkg/auth v0.27.0
github.com/fluxcd/pkg/cache v0.10.0
github.com/fluxcd/pkg/git v0.35.0
github.com/fluxcd/pkg/masktoken v0.7.0
github.com/fluxcd/pkg/runtime v0.80.0
github.com/fluxcd/pkg/ssa v0.51.0
github.com/fluxcd/pkg/ssh v0.20.0
github.com/getsentry/sentry-go v0.34.1
github.com/go-logr/logr v1.4.3
github.com/google/cel-go v0.26.0
github.com/google/go-github/v64 v64.0.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/ktrysmt/go-bitbucket v0.9.86
github.com/containrrr/shoutrrr v0.8.0
github.com/fluxcd/cli-utils v0.36.0-flux.2
github.com/fluxcd/notification-controller/api v1.2.2
github.com/fluxcd/pkg/apis/event v0.6.0
github.com/fluxcd/pkg/apis/meta v1.2.0
github.com/fluxcd/pkg/git v0.16.0
github.com/fluxcd/pkg/masktoken v0.2.0
github.com/fluxcd/pkg/runtime v0.43.2
github.com/fluxcd/pkg/ssa v0.35.0
github.com/getsentry/sentry-go v0.25.0
github.com/go-logr/logr v1.3.0
github.com/google/go-github/v53 v53.2.0
github.com/hashicorp/go-retryablehttp v0.7.5
github.com/ktrysmt/go-bitbucket v0.9.73
github.com/microsoft/azure-devops-go-api/azuredevops/v6 v6.0.1
github.com/nats-io/nats.go v1.43.0
github.com/onsi/gomega v1.38.0
github.com/sethvargo/go-limiter v1.0.0
github.com/slok/go-http-metrics v0.13.0
github.com/spf13/pflag v1.0.7
github.com/stretchr/testify v1.10.0
gitlab.com/gitlab-org/api/client-go v0.137.0
golang.org/x/oauth2 v0.30.0
golang.org/x/text v0.27.0
google.golang.org/api v0.243.0
k8s.io/api v0.33.2
k8s.io/apimachinery v0.33.2
k8s.io/client-go v0.33.2
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
sigs.k8s.io/controller-runtime v0.21.0
sigs.k8s.io/yaml v1.5.0
github.com/nats-io/nats.go v1.31.0
github.com/onsi/gomega v1.30.0
github.com/sethvargo/go-limiter v0.7.2
github.com/slok/go-http-metrics v0.11.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
github.com/xanzy/go-gitlab v0.95.1
golang.org/x/oauth2 v0.15.0
golang.org/x/text v0.14.0
google.golang.org/api v0.153.0
k8s.io/api v0.28.4
k8s.io/apimachinery v0.28.4
k8s.io/client-go v0.28.4
k8s.io/utils v0.0.0-20231127182322-b307cd553661
sigs.k8s.io/controller-runtime v0.16.3
sigs.k8s.io/yaml v1.4.0
)
// Fix CVE-2022-28948
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
require (
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go v0.120.0 // indirect
cloud.google.com/go/auth v0.16.3 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
github.com/42wim/httpsig v1.2.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0 // indirect
github.com/Azure/go-amqp v1.4.0 // indirect
cloud.google.com/go v0.110.10 // indirect
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.5 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/go-amqp v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/DataDog/zstd v1.5.2 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20231012073058-a7379d079e0e // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bradleyfalzon/ghinstallation/v2 v2.16.0 // indirect
github.com/carapace-sh/carapace-shlex v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cloudevents/sdk-go/v2 v2.15.2 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/cloudflare/circl v1.3.6 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/docker/cli v28.2.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/devigned/tab v0.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.7.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fluxcd/pkg/apis/acl v0.7.0 // indirect
github.com/fluxcd/pkg/apis/kustomize v1.11.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-containerregistry v0.20.6 // indirect
github.com/google/go-github/v72 v72.0.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/klauspost/compress v1.17.1 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nkeys v0.4.6 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/package-url/packageurl-go v0.1.1 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/stoewer/go-strcase v1.3.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.starlark.net v0.0.0-20231121155337-90ade8b19d09 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/term v0.33.0 // indirect
golang.org/x/time v0.12.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/evanphx/json-patch.v5 v5.7.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/apiextensions-apiserver v0.33.2 // indirect
k8s.io/cli-runtime v0.33.2 // indirect
k8s.io/component-base v0.33.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911 // indirect
k8s.io/kubectl v0.33.2 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/kustomize/api v0.20.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.20.0 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
k8s.io/apiextensions-apiserver v0.28.4 // indirect
k8s.io/cli-runtime v0.28.4 // indirect
k8s.io/component-base v0.28.4 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231206194836-bf4651e18aa8 // indirect
k8s.io/kubectl v0.28.4 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.16.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.16.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
)

2069
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@ package controller
import (
"context"
corev1 "k8s.io/api/core/v1"
kuberecorder "k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
@ -27,16 +28,12 @@ import (
apiv1 "github.com/fluxcd/notification-controller/api/v1"
apiv1beta3 "github.com/fluxcd/notification-controller/api/v1beta3"
"github.com/fluxcd/pkg/cache"
"github.com/fluxcd/pkg/runtime/patch"
"github.com/fluxcd/notification-controller/internal/notifier"
)
// +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=providers,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
// ProviderReconciler reconciles a Provider object to migrate it to static
// Provider.
@ -44,21 +41,40 @@ type ProviderReconciler struct {
client.Client
kuberecorder.EventRecorder
TokenCache *cache.TokenCache
ControllerName string
}
func (r *ProviderReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&apiv1beta3.Provider{}, builder.WithPredicates(providerPredicate{})).
For(&apiv1beta3.Provider{}, builder.WithPredicates(finalizerPredicate{})).
Complete(r)
}
func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
log := ctrl.LoggerFrom(ctx)
obj := &apiv1beta3.Provider{}
if err := r.Get(ctx, req.NamespacedName, obj); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Early return if no migration is needed.
if !controllerutil.ContainsFinalizer(obj, apiv1.NotificationFinalizer) {
return ctrl.Result{}, nil
}
// Examine if the object is under deletion.
var delete bool
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
delete = true
}
// Skip if it's suspend and not being deleted.
if obj.Spec.Suspend && !delete {
log.Info("reconciliation is suspended for this object")
return ctrl.Result{}, nil
}
patcher, err := patch.NewHelper(obj, r.Client)
if err != nil {
return ctrl.Result{}, err
@ -70,29 +86,11 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r
}
}()
// Examine if the object is under deletion.
if !obj.ObjectMeta.DeletionTimestamp.IsZero() {
return r.reconcileDelete(obj)
}
// Remove the notification-controller finalizer.
controllerutil.RemoveFinalizer(obj, apiv1.NotificationFinalizer)
// Add finalizer if it doesn't exist.
if !controllerutil.ContainsFinalizer(obj, apiv1.NotificationFinalizer) {
controllerutil.AddFinalizer(obj, apiv1.NotificationFinalizer)
}
log.Info("removed finalizer from Provider to migrate to static Provider")
r.Event(obj, corev1.EventTypeNormal, "Migration", "removed finalizer from Provider to migrate to static Provider")
return
}
// reconcileDelete handles the deletion of the object.
// It cleans up the caches and removes the finalizer.
func (r *ProviderReconciler) reconcileDelete(obj *apiv1beta3.Provider) (ctrl.Result, error) {
// Remove our finalizer from the list
controllerutil.RemoveFinalizer(obj, apiv1.NotificationFinalizer)
// Cleanup caches.
r.TokenCache.DeleteEventsForObject(apiv1beta3.ProviderKind,
obj.GetName(), obj.GetNamespace(), notifier.OperationPost)
// Stop reconciliation as the object is being deleted
return ctrl.Result{}, nil
}

View File

@ -52,30 +52,62 @@ func TestProviderReconciler(t *testing.T) {
}
providerKey := client.ObjectKeyFromObject(provider)
// Create without finalizer.
// Remove finalizer at create.
provider.ObjectMeta.Finalizers = append(provider.ObjectMeta.Finalizers, "foo.bar", apiv1.NotificationFinalizer)
provider.Spec = apiv1beta3.ProviderSpec{
Type: "generic",
Type: "slack",
}
g.Expect(testEnv.Create(ctx, provider)).ToNot(HaveOccurred())
// Should eventually have finalizer.
g.Eventually(func() bool {
_ = testEnv.Get(ctx, providerKey, provider)
return controllerutil.ContainsFinalizer(provider, apiv1.NotificationFinalizer)
return !controllerutil.ContainsFinalizer(provider, apiv1.NotificationFinalizer)
}, timeout, time.Second).Should(BeTrue())
// Remove finalizer.
// Remove finalizer at update.
patchHelper, err := patch.NewHelper(provider, testEnv.Client)
g.Expect(err).ToNot(HaveOccurred())
controllerutil.RemoveFinalizer(provider, apiv1.NotificationFinalizer)
provider.ObjectMeta.Finalizers = append(provider.ObjectMeta.Finalizers, apiv1.NotificationFinalizer)
g.Expect(patchHelper.Patch(ctx, provider)).ToNot(HaveOccurred())
// Should eventually have finalizer again.
g.Eventually(func() bool {
_ = testEnv.Get(ctx, providerKey, provider)
return controllerutil.ContainsFinalizer(provider, apiv1.NotificationFinalizer)
return !controllerutil.ContainsFinalizer(provider, apiv1.NotificationFinalizer)
}, timeout, time.Second).Should(BeTrue())
// Remove finalizer at delete.
patchHelper, err = patch.NewHelper(provider, testEnv.Client)
g.Expect(err).ToNot(HaveOccurred())
// Suspend the provider to prevent finalizer from getting removed.
// Ensure only flux finalizer is set to allow the object to be garbage
// collected at the end.
// NOTE: Suspending and updating finalizers are done separately here as
// doing them in a single patch results in flaky test where the finalizer
// update doesn't gets registered with the kube-apiserver, resulting in
// timeout waiting for finalizer to appear on the object below.
provider.Spec.Suspend = true
g.Expect(patchHelper.Patch(ctx, provider)).ToNot(HaveOccurred())
g.Eventually(func() bool {
_ = k8sClient.Get(ctx, providerKey, provider)
return provider.Spec.Suspend == true
}, timeout).Should(BeTrue())
patchHelper, err = patch.NewHelper(provider, testEnv.Client)
g.Expect(err).ToNot(HaveOccurred())
// Add finalizer and verify that finalizer exists on the object using a live
// client.
provider.ObjectMeta.Finalizers = []string{apiv1.NotificationFinalizer}
g.Expect(patchHelper.Patch(ctx, provider)).ToNot(HaveOccurred())
g.Eventually(func() bool {
_ = k8sClient.Get(ctx, providerKey, provider)
return controllerutil.ContainsFinalizer(provider, apiv1.NotificationFinalizer)
}, timeout).Should(BeTrue())
// Delete the object and verify.
g.Expect(testEnv.Delete(ctx, provider)).ToNot(HaveOccurred())
g.Eventually(func() bool {
@ -85,85 +117,3 @@ func TestProviderReconciler(t *testing.T) {
return false
}, timeout).Should(BeTrue())
}
func TestProviderReconciler_APIServerValidation(t *testing.T) {
tests := []struct {
name string
providerType string
commitStatusExpr string
err string
}{
{
name: "github provider types can create providers with commitStatusExpr",
providerType: "github",
commitStatusExpr: "event.metadata.namespace + '/' + event.metadata.name + '/' + provider.metadata.uid",
},
{
name: "gitlab provider types can create providers with commitStatusExpr",
providerType: "gitlab",
commitStatusExpr: "event.metadata.namespace + '/' + event.metadata.name + '/' + provider.metadata.uid",
},
{
name: "gitea provider types can create providers with commitStatusExpr",
providerType: "gitea",
commitStatusExpr: "event.metadata.namespace + '/' + event.metadata.name + '/' + provider.metadata.uid",
},
{
name: "bitbucketserver provider types can create providers with commitStatusExpr",
providerType: "bitbucketserver",
commitStatusExpr: "event.metadata.namespace + '/' + event.metadata.name + '/' + provider.metadata.uid",
},
{
name: "bitbucket provider types can create providers with commitStatusExpr",
providerType: "bitbucket",
commitStatusExpr: "event.metadata.namespace + '/' + event.metadata.name + '/' + provider.metadata.uid",
},
{
name: "azuredevops provider types can create providers with commitStatusExpr",
providerType: "azuredevops",
commitStatusExpr: "event.metadata.namespace + '/' + event.metadata.name + '/' + provider.metadata.uid",
},
{
name: "unsupported provider types cannot create providers with commitStatusExpr",
providerType: "slack",
commitStatusExpr: "event.metadata.namespace + '/' + event.metadata.name + '/' + provider.metadata.uid",
err: "spec.commitStatusExpr is only supported for the 'github', 'gitlab', 'gitea', 'bitbucketserver', 'bitbucket', 'azuredevops' provider types",
},
{
name: "github provider types can create providers without commitStatusExpr",
providerType: "github",
commitStatusExpr: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
obj := &apiv1beta3.Provider{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "provider-reconcile-",
Namespace: "default",
},
Spec: apiv1beta3.ProviderSpec{
Type: tt.providerType,
CommitStatusExpr: tt.commitStatusExpr,
},
}
err := testEnv.Create(ctx, obj)
if err == nil {
defer func() {
err := testEnv.Delete(ctx, obj)
g.Expect(err).ToNot(HaveOccurred())
}()
}
if tt.err != "" {
g.Expect(err.Error()).To(ContainSubstring(tt.err))
} else {
g.Expect(err).NotTo(HaveOccurred())
}
})
}
}

View File

@ -1,48 +0,0 @@
/*
Copyright 2025 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 controller
import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
apiv1 "github.com/fluxcd/notification-controller/api/v1"
apiv1beta3 "github.com/fluxcd/notification-controller/api/v1beta3"
)
// providerPredicate implements predicate functions for the Provider API.
type providerPredicate struct{}
func (providerPredicate) Create(e event.CreateEvent) bool {
return !controllerutil.ContainsFinalizer(e.Object, apiv1.NotificationFinalizer)
}
func (providerPredicate) Update(e event.UpdateEvent) bool {
if e.ObjectNew == nil {
return false
}
return !controllerutil.ContainsFinalizer(e.ObjectNew, apiv1.NotificationFinalizer) ||
!e.ObjectNew.(*apiv1beta3.Provider).ObjectMeta.DeletionTimestamp.IsZero()
}
func (providerPredicate) Delete(e event.DeleteEvent) bool {
return false
}
func (providerPredicate) Generic(e event.GenericEvent) bool {
return !controllerutil.ContainsFinalizer(e.Object, apiv1.NotificationFinalizer)
}

View File

@ -26,15 +26,13 @@ import (
"k8s.io/apimachinery/pkg/types"
kerrors "k8s.io/apimachinery/pkg/util/errors"
kuberecorder "k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/ratelimiter"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
@ -56,20 +54,13 @@ type ReceiverReconciler struct {
}
type ReceiverReconcilerOptions struct {
RateLimiter workqueue.TypedRateLimiter[reconcile.Request]
WatchConfigsPredicate predicate.Predicate
RateLimiter ratelimiter.RateLimiter
}
func (r *ReceiverReconciler) SetupWithManager(mgr ctrl.Manager) error {
return r.SetupWithManagerAndOptions(mgr, ReceiverReconcilerOptions{
WatchConfigsPredicate: predicate.Not(predicate.Funcs{}),
})
return r.SetupWithManagerAndOptions(mgr, ReceiverReconcilerOptions{})
}
const (
secretRefIndex = ".metadata.secretRef"
)
func (r *ReceiverReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts ReceiverReconcilerOptions) error {
// This index is used to list Receivers by their webhook path after the receiver server
// gets a request.
@ -77,60 +68,16 @@ func (r *ReceiverReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, opts R
server.WebhookPathIndexKey, server.IndexReceiverWebhookPath); err != nil {
return err
}
// Index receivers by the secret reference, so that we can enqueue
// Receiver requests when the referenced Secret is changed.
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &apiv1.Receiver{},
secretRefIndex, func(obj client.Object) []string {
receiver := obj.(*apiv1.Receiver)
return []string{fmt.Sprintf("%s/%s", receiver.GetNamespace(), receiver.Spec.SecretRef.Name)}
}); err != nil {
}
return ctrl.NewControllerManagedBy(mgr).
For(&apiv1.Receiver{}, builder.WithPredicates(
predicate.Or(predicate.GenerationChangedPredicate{}, predicates.ReconcileRequestedPredicate{}),
)).
WatchesMetadata(
&corev1.Secret{},
handler.EnqueueRequestsFromMapFunc(r.enqueueRequestsForChangeOf),
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, opts.WatchConfigsPredicate),
).
WithOptions(controller.Options{
RateLimiter: opts.RateLimiter,
}).
Complete(r)
}
// enqueueRequestsForChangeOf enqueues Receiver requests for changes in referenced Secret objects.
func (r *ReceiverReconciler) enqueueRequestsForChangeOf(ctx context.Context, obj client.Object) []reconcile.Request {
log := ctrl.LoggerFrom(ctx)
// List all Receivers that have the referenced Secret in their spec.
receivers := &apiv1.ReceiverList{}
if err := r.List(ctx, receivers, client.MatchingFields{
secretRefIndex: client.ObjectKeyFromObject(obj).String(),
}); err != nil {
log.Error(err, "failed to list Receivers for change of Secret",
"secretRef", map[string]string{
"name": obj.GetName(),
"namespace": obj.GetNamespace(),
})
return nil
}
requests := make([]reconcile.Request, 0, len(receivers.Items))
for _, receiver := range receivers.Items {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: receiver.Name,
Namespace: receiver.Namespace,
},
})
}
return requests
}
// +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=receivers,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=notification.toolkit.fluxcd.io,resources=receivers/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=source.fluxcd.io,resources=buckets,verbs=get;list;watch;update;patch
@ -165,7 +112,9 @@ func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r
}
// Record Prometheus metrics.
r.Metrics.RecordReadiness(ctx, obj)
r.Metrics.RecordDuration(ctx, obj, reconcileStart)
r.Metrics.RecordSuspend(ctx, obj, obj.Spec.Suspend)
// Emit warning event if the reconciliation failed.
if retErr != nil {
@ -208,40 +157,25 @@ func (r *ReceiverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (r
// reconcile steps through the actual reconciliation tasks for the object, it returns early on the first step that
// produces an error.
func (r *ReceiverReconciler) reconcile(ctx context.Context, obj *apiv1.Receiver) (ctrl.Result, error) {
log := ctrl.LoggerFrom(ctx)
if filter := obj.Spec.ResourceFilter; filter != "" {
if err := server.ValidateResourceFilter(filter); err != nil {
const msg = "Reconciliation failed terminally due to configuration error"
errMsg := fmt.Sprintf("%s: %v", msg, err)
conditions.MarkFalse(obj, meta.ReadyCondition, meta.InvalidCELExpressionReason, "%s", errMsg)
conditions.MarkStalled(obj, meta.InvalidCELExpressionReason, "%s", errMsg)
obj.Status.ObservedGeneration = obj.Generation
log.Error(err, msg)
r.Event(obj, corev1.EventTypeWarning, meta.InvalidCELExpressionReason, errMsg)
return ctrl.Result{}, nil
}
}
// Mark the resource as under reconciliation.
conditions.MarkReconciling(obj, meta.ProgressingReason, "Reconciliation in progress")
token, err := r.token(ctx, obj)
if err != nil {
conditions.MarkFalse(obj, meta.ReadyCondition, apiv1.TokenNotFoundReason, "%s", err)
conditions.MarkFalse(obj, meta.ReadyCondition, apiv1.TokenNotFoundReason, err.Error())
obj.Status.WebhookPath = ""
return ctrl.Result{}, err
return ctrl.Result{Requeue: true}, err
}
webhookPath := obj.GetWebhookPath(token)
msg := fmt.Sprintf("Receiver initialized for path: %s", webhookPath)
// Mark the resource as ready and set the webhook path in status.
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "%s", msg)
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, msg)
if obj.Status.WebhookPath != webhookPath {
obj.Status.WebhookPath = webhookPath
log.Info(msg)
ctrl.LoggerFrom(ctx).Info(msg)
}
return ctrl.Result{RequeueAfter: obj.GetInterval()}, nil

View File

@ -144,45 +144,6 @@ func TestReceiverReconciler_Reconcile(t *testing.T) {
g.Expect(resultR.Spec.Interval.Duration).To(BeIdenticalTo(10 * time.Minute))
})
t.Run("fails with invalid CEL resource filter", func(t *testing.T) {
g := NewWithT(t)
g.Expect(k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR)).To(Succeed())
// Incomplete CEL expression
patch := []byte(`{"spec":{"resourceFilter":"has(res.metadata.annotations"}}`)
g.Expect(k8sClient.Patch(context.Background(), resultR, client.RawPatch(types.MergePatchType, patch))).To(Succeed())
g.Eventually(func() bool {
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR)
return !conditions.IsReady(resultR)
}, timeout, time.Second).Should(BeTrue())
g.Expect(resultR.Status.ObservedGeneration).To(Equal(resultR.Generation))
g.Expect(conditions.GetReason(resultR, meta.ReadyCondition)).To(BeIdenticalTo(meta.InvalidCELExpressionReason))
g.Expect(conditions.GetMessage(resultR, meta.ReadyCondition)).To(ContainSubstring("annotations"))
g.Expect(conditions.Has(resultR, meta.StalledCondition)).To(BeTrue())
g.Expect(conditions.GetReason(resultR, meta.StalledCondition)).To(BeIdenticalTo(meta.InvalidCELExpressionReason))
g.Expect(conditions.GetObservedGeneration(resultR, meta.StalledCondition)).To(BeIdenticalTo(resultR.Generation))
})
t.Run("recovers when the CEL expression is valid", func(t *testing.T) {
g := NewWithT(t)
// Incomplete CEL expression
patch := []byte(`{"spec":{"resourceFilter":"has(res.metadata.annotations)"}}`)
g.Expect(k8sClient.Patch(context.Background(), resultR, client.RawPatch(types.MergePatchType, patch))).To(Succeed())
g.Eventually(func() bool {
_ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(receiver), resultR)
return conditions.IsReady(resultR)
}, timeout, time.Second).Should(BeTrue())
g.Expect(conditions.GetObservedGeneration(resultR, meta.ReadyCondition)).To(BeIdenticalTo(resultR.Generation))
g.Expect(resultR.Status.ObservedGeneration).To(BeIdenticalTo(resultR.Generation))
g.Expect(conditions.Has(resultR, meta.ReconcilingCondition)).To(BeFalse())
})
t.Run("fails with secret not found error", func(t *testing.T) {
g := NewWithT(t)
@ -278,7 +239,7 @@ func TestReceiverReconciler_EventHandler(t *testing.T) {
// Use the client from the manager as the server handler needs to list objects from the cache
// which the "live" k8s client does not have access to.
receiverServer := server.NewReceiverServer("127.0.0.1:56788", logf.Log, testEnv.GetClient(), true, true)
receiverServer := server.NewReceiverServer("127.0.0.1:56788", logf.Log, testEnv.GetClient())
receiverMdlw := middleware.New(middleware.Config{
Recorder: prommetrics.NewRecorder(prommetrics.Config{
Prefix: "gotk_receiver",

View File

@ -33,7 +33,6 @@ import (
"k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"github.com/fluxcd/cli-utils/pkg/kstatus/polling"
runtimeclient "github.com/fluxcd/pkg/runtime/client"
@ -41,7 +40,6 @@ import (
"github.com/fluxcd/pkg/runtime/metrics"
"github.com/fluxcd/pkg/runtime/testenv"
"github.com/fluxcd/pkg/ssa"
ssautil "github.com/fluxcd/pkg/ssa/utils"
apiv1 "github.com/fluxcd/notification-controller/api/v1"
apiv1b2 "github.com/fluxcd/notification-controller/api/v1beta2"
@ -83,8 +81,9 @@ func TestMain(m *testing.M) {
}
if err := (&ProviderReconciler{
Client: testEnv,
EventRecorder: testEnv.GetEventRecorderFor(controllerName),
Client: testEnv,
ControllerName: controllerName,
EventRecorder: testEnv.GetEventRecorderFor(controllerName),
}).SetupWithManager(testEnv); err != nil {
panic(fmt.Sprintf("Failed to start ProviderReconciler: %v", err))
}
@ -95,8 +94,7 @@ func TestMain(m *testing.M) {
ControllerName: controllerName,
EventRecorder: testEnv.GetEventRecorderFor(controllerName),
}).SetupWithManagerAndOptions(testEnv, ReceiverReconcilerOptions{
RateLimiter: controller.GetDefaultRateLimiter(),
WatchConfigsPredicate: predicate.Not(predicate.Funcs{}),
RateLimiter: controller.GetDefaultRateLimiter(),
}); err != nil {
panic(fmt.Sprintf("Failed to start ReceiverReconciler: %v", err))
}
@ -157,7 +155,7 @@ func readManifest(manifest, namespace string) (*unstructured.Unstructured, error
}
yml := fmt.Sprintf(string(data), namespace)
object, err := ssautil.ReadObject(strings.NewReader(yml))
object, err := ssa.ReadObject(strings.NewReader(yml))
if err != nil {
return nil, err
}

View File

@ -18,10 +18,7 @@ limitations under the License.
// and their default states.
package features
import (
"github.com/fluxcd/pkg/auth"
feathelper "github.com/fluxcd/pkg/runtime/features"
)
import feathelper "github.com/fluxcd/pkg/runtime/features"
const (
// CacheSecretsAndConfigMaps controls whether Secrets and ConfigMaps should
@ -38,10 +35,6 @@ var features = map[string]bool{
CacheSecretsAndConfigMaps: false,
}
func init() {
auth.SetFeatureGates(features)
}
// FeatureGates contains a list of all supported feature gates and
// their default values.
func FeatureGates() map[string]bool {

View File

@ -18,13 +18,10 @@ package notifier
import (
"context"
"crypto/tls"
"encoding/json"
"crypto/x509"
"fmt"
"net/url"
"time"
"github.com/hashicorp/go-retryablehttp"
"golang.org/x/text/cases"
"golang.org/x/text/language"
@ -32,63 +29,27 @@ import (
)
type Alertmanager struct {
URL string
ProxyURL string
TLSConfig *tls.Config
Token string
Username string
Password string
URL string
ProxyURL string
CertPool *x509.CertPool
}
type AlertManagerAlert struct {
Status string `json:"status"`
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
StartsAt AlertManagerTime `json:"startsAt"`
EndsAt AlertManagerTime `json:"endsAt,omitempty"`
}
// AlertManagerTime takes care of representing time.Time as RFC3339.
// See https://prometheus.io/docs/alerting/0.27/clients/
type AlertManagerTime time.Time
func (a AlertManagerTime) String() string {
return time.Time(a).Format(time.RFC3339)
}
func (a AlertManagerTime) MarshalJSON() ([]byte, error) {
return json.Marshal(a.String())
}
func (a *AlertManagerTime) UnmarshalJSON(jsonRepr []byte) error {
var serializedTime string
if err := json.Unmarshal(jsonRepr, &serializedTime); err != nil {
return err
}
t, err := time.Parse(time.RFC3339, serializedTime)
if err != nil {
return err
}
*a = AlertManagerTime(t)
return nil
}
func NewAlertmanager(hookURL string, proxyURL string, tlsConfig *tls.Config, token, user, pass string) (*Alertmanager, error) {
func NewAlertmanager(hookURL string, proxyURL string, certPool *x509.CertPool) (*Alertmanager, error) {
_, err := url.ParseRequestURI(hookURL)
if err != nil {
return nil, fmt.Errorf("invalid Alertmanager URL %s: '%w'", hookURL, err)
}
return &Alertmanager{
URL: hookURL,
ProxyURL: proxyURL,
Token: token,
Username: user,
Password: pass,
TLSConfig: tlsConfig,
URL: hookURL,
ProxyURL: proxyURL,
CertPool: certPool,
}, nil
}
@ -114,54 +75,25 @@ func (s *Alertmanager) Post(ctx context.Context, event eventv1.Event) error {
labels["alertname"] = "Flux" + event.InvolvedObject.Kind + cases.Title(language.Und).String(event.Reason)
labels["severity"] = event.Severity
labels["reason"] = event.Reason
labels["timestamp"] = event.Timestamp.String()
labels["kind"] = event.InvolvedObject.Kind
labels["name"] = event.InvolvedObject.Name
labels["namespace"] = event.InvolvedObject.Namespace
labels["reportingcontroller"] = event.ReportingController
// The best reasonable `endsAt` value would be multiplying
// InvolvedObject's reconciliation interval by 2 then adding that to
// `startsAt` (the next successful reconciliation would make sure
// the alert is cleared after the timeout). Due to
// event.InvolvedObject only containing the object reference (namely
// the GVKNN) best we can do is leave it unset up to Alertmanager's
// default `resolve_timeout`.
//
// https://prometheus.io/docs/alerting/0.27/configuration/#file-layout-and-global-settings
startsAt := AlertManagerTime(event.Timestamp.Time)
payload := []AlertManagerAlert{
{
Labels: labels,
Annotations: annotations,
Status: "firing",
StartsAt: startsAt,
},
}
var opts []postOption
if s.ProxyURL != "" {
opts = append(opts, withProxy(s.ProxyURL))
}
if s.TLSConfig != nil {
opts = append(opts, withTLSConfig(s.TLSConfig))
}
if s.Token != "" {
opts = append(opts, withRequestModifier(func(request *retryablehttp.Request) {
request.Header.Add("Authorization", "Bearer "+s.Token)
}))
}
if s.Username != "" && s.Password != "" {
opts = append(opts, withRequestModifier(func(request *retryablehttp.Request) {
request.SetBasicAuth(s.Username, s.Password)
}))
}
err := postMessage(ctx, s.URL, s.ProxyURL, s.CertPool, payload)
if err := postMessage(ctx, s.URL, payload, opts...); err != nil {
if err != nil {
return fmt.Errorf("postMessage failed: %w", err)
}
return nil
}

View File

@ -18,7 +18,7 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
@ -43,10 +43,10 @@ func Fuzz_AlertManager(f *testing.F) {
}))
defer ts.Close()
var tlsConfig tls.Config
_ = fuzz.NewConsumer(seed).GenerateStruct(&tlsConfig)
var cert x509.CertPool
_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
alertmanager, err := NewAlertmanager(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", &tlsConfig, "", "", "")
alertmanager, err := NewAlertmanager(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", &cert)
if err != nil {
return
}

View File

@ -38,7 +38,7 @@ func TestAlertmanager_Post(t *testing.T) {
}))
defer ts.Close()
alertmanager, err := NewAlertmanager(ts.URL, "", nil, "", "", "")
alertmanager, err := NewAlertmanager(ts.URL, "", nil)
require.NoError(t, err)
err = alertmanager.Post(context.TODO(), testEvent())

View File

@ -19,18 +19,16 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strings"
"github.com/microsoft/azure-devops-go-api/azuredevops/v6"
"github.com/microsoft/azure-devops-go-api/azuredevops/v6/git"
"sigs.k8s.io/controller-runtime/pkg/client"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/auth/azure"
"github.com/fluxcd/pkg/cache"
)
const genre string = "fluxcd"
@ -42,23 +40,16 @@ type azureDevOpsClient interface {
// AzureDevOps is an Azure DevOps notifier.
type AzureDevOps struct {
Project string
Repo string
CommitStatus string
Client azureDevOpsClient
Project string
Repo string
ProviderUID string
Client azureDevOpsClient
}
// NewAzureDevOps creates and returns a new AzureDevOps notifier.
func NewAzureDevOps(ctx context.Context, commitStatus string, addr string, token string,
tlsConfig *tls.Config, proxy, serviceAccountName, providerName, providerNamespace string,
tokenClient client.Client, tokenCache *cache.TokenCache) (*AzureDevOps, error) {
var err error
func NewAzureDevOps(providerUID string, addr string, token string, certPool *x509.CertPool) (*AzureDevOps, error) {
if len(token) == 0 {
// if token doesn't exist, try to create a new token using managed identity
token, err = newManagedIdentityToken(ctx, proxy, serviceAccountName, providerName, providerNamespace, azure.ScopeDevOps, tokenClient, tokenCache)
if err != nil {
return nil, fmt.Errorf("failed to acquire azure devops token: %w", err)
}
return nil, errors.New("azure devops token cannot be empty")
}
host, id, err := parseGitAddress(addr)
@ -66,11 +57,6 @@ func NewAzureDevOps(ctx context.Context, commitStatus string, addr string, token
return nil, err
}
// this should never happen
if commitStatus == "" {
return nil, errors.New("commit status cannot be empty")
}
comp := strings.Split(id, "/")
if len(comp) != 4 {
return nil, fmt.Errorf("invalid repository id %q", id)
@ -81,18 +67,20 @@ func NewAzureDevOps(ctx context.Context, commitStatus string, addr string, token
orgURL := fmt.Sprintf("%v/%v", host, org)
connection := azuredevops.NewPatConnection(orgURL, token)
if tlsConfig != nil {
connection.TlsConfig = tlsConfig
if certPool != nil {
connection.TlsConfig = &tls.Config{
RootCAs: certPool,
}
}
client := connection.GetClientByUrl(orgURL)
gitClient := &git.ClientImpl{
Client: *client,
}
return &AzureDevOps{
Project: proj,
Repo: repo,
CommitStatus: commitStatus,
Client: gitClient,
Project: proj,
Repo: repo,
ProviderUID: providerUID,
Client: gitClient,
}, nil
}
@ -103,7 +91,7 @@ func (a AzureDevOps) Post(ctx context.Context, event eventv1.Event) error {
return nil
}
revString, ok := event.GetRevision()
revString, ok := event.Metadata[eventv1.MetaRevisionKey]
if !ok {
return errors.New("missing revision metadata")
}
@ -120,7 +108,7 @@ func (a AzureDevOps) Post(ctx context.Context, event eventv1.Event) error {
g := commitStatusGenre(event)
_, desc := formatNameAndDescription(event)
id := a.CommitStatus
id := generateCommitStatusID(a.ProviderUID, event)
createArgs := git.CreateCommitStatusArgs{
Project: &a.Project,
RepositoryId: &a.Repo,

View File

@ -18,7 +18,6 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
@ -34,12 +33,12 @@ import (
const apiLocations = `{"count":0,"value":[{"area":"","id":"428dd4fb-fda5-4722-af02-9313b80305da","routeTemplate":"","resourceName":"","maxVersion":"6.0","minVersion":"5.0","releasedVersion":"6.0"}]}`
func Fuzz_AzureDevOps(f *testing.F) {
f.Add("kustomization/gitops-system/0c9c2e41", "alakazam", "org/proj/_git/repo", "revision/dsa123a", "error", "", []byte{}, []byte(`{"count":1,"value":[{"state":"error","description":"","context":{"genre":"fluxcd","name":"/"}}]}`))
f.Add("kustomization/gitops-system/0c9c2e41", "alakazam", "org/proj/_git/repo", "revision/dsa123a", "info", "", []byte{}, []byte(`{"count":1,"value":[{"state":"info","description":"","context":{"genre":"fluxcd","name":"/"}}]}`))
f.Add("kustomization/gitops-system/0c9c2e41", "alakazam", "org/proj/_git/repo", "revision/dsa123a", "info", "", []byte{}, []byte(`{"count":0,"value":[]}`))
f.Add("kustomization/gitops-system/0c9c2e41", "alakazam", "org/proj/_git/repo", "", "", "Progressing", []byte{}, []byte{})
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "alakazam", "org/proj/_git/repo", "revision/dsa123a", "error", "", []byte{}, []byte(`{"count":1,"value":[{"state":"error","description":"","context":{"genre":"fluxcd","name":"/"}}]}`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "alakazam", "org/proj/_git/repo", "revision/dsa123a", "info", "", []byte{}, []byte(`{"count":1,"value":[{"state":"info","description":"","context":{"genre":"fluxcd","name":"/"}}]}`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "alakazam", "org/proj/_git/repo", "revision/dsa123a", "info", "", []byte{}, []byte(`{"count":0,"value":[]}`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "alakazam", "org/proj/_git/repo", "", "", "Progressing", []byte{}, []byte{})
f.Fuzz(func(t *testing.T, commitStatus, token, urlSuffix, revision, severity, reason string, seed, response []byte) {
f.Fuzz(func(t *testing.T, uuid, token, urlSuffix, revision, severity, reason string, seed, response []byte) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "_apis") {
w.Write([]byte(apiLocations))
@ -55,8 +54,7 @@ func Fuzz_AzureDevOps(f *testing.F) {
var cert x509.CertPool
_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
tlsConfig := &tls.Config{RootCAs: &cert}
azureDevOps, err := NewAzureDevOps(context.TODO(), commitStatus, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, tlsConfig, "", "", "", "", nil, nil)
azureDevOps, err := NewAzureDevOps(uuid, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert)
if err != nil {
return
}

View File

@ -27,24 +27,19 @@ import (
)
func TestNewAzureDevOpsBasic(t *testing.T) {
a, err := NewAzureDevOps(context.TODO(), "kustomization/gitops-system/0c9c2e41", "https://dev.azure.com/foo/bar/_git/baz", "foo", nil, "", "", "", "", nil, nil)
a, err := NewAzureDevOps("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://dev.azure.com/foo/bar/_git/baz", "foo", nil)
assert.Nil(t, err)
assert.Equal(t, a.Project, "bar")
assert.Equal(t, a.Repo, "baz")
}
func TestNewAzureDevOpsInvalidUrl(t *testing.T) {
_, err := NewAzureDevOps(context.TODO(), "kustomization/gitops-system/0c9c2e41", "https://dev.azure.com/foo/bar/baz", "foo", nil, "", "", "", "", nil, nil)
_, err := NewAzureDevOps("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://dev.azure.com/foo/bar/baz", "foo", nil)
assert.NotNil(t, err)
}
func TestNewAzureDevOpsMissingToken(t *testing.T) {
_, err := NewAzureDevOps(context.TODO(), "kustomization/gitops-system/0c9c2e41", "https://dev.azure.com/foo/bar/baz", "", nil, "", "", "", "", nil, nil)
assert.NotNil(t, err)
}
func TestNewAzureDevOpsEmptyCommitStatus(t *testing.T) {
_, err := NewAzureDevOps(context.TODO(), "", "https://dev.azure.com/foo/bar/_git/baz", "foo", nil, "", "", "", "", nil, nil)
_, err := NewAzureDevOps("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://dev.azure.com/foo/bar/baz", "", nil)
assert.NotNil(t, err)
}
@ -105,34 +100,6 @@ func TestAzureDevOps_Post(t *testing.T) {
},
},
},
{
name: "event with origin revision",
event: eventv1.Event{
Severity: eventv1.EventSeverityInfo,
InvolvedObject: corev1.ObjectReference{
Kind: "Kustomization",
Name: "gitops-system",
},
Metadata: map[string]string{
eventv1.MetaRevisionKey: "main@sha1:69b59063470310ebbd88a9156325322a124e55a3",
eventv1.MetaOriginRevisionKey: "main@sha1:bd88a9156325322a124e55a369b59063470310eb",
},
Reason: "ApplySucceeded",
},
want: git.CreateCommitStatusArgs{
CommitId: strPtr("bd88a9156325322a124e55a369b59063470310eb"),
Project: strPtr("bar"),
RepositoryId: strPtr("baz"),
GitCommitStatusToCreate: &git.GitStatus{
Description: strPtr("apply succeeded"),
State: &git.GitStatusStateValues.Succeeded,
Context: &git.GitStatusContext{
Genre: strPtr("fluxcd"),
Name: strPtr("kustomization/gitops-system/0c9c2e41"),
},
},
},
},
{
name: "event with summary",
event: eventv1.Event{
@ -165,7 +132,7 @@ func TestAzureDevOps_Post(t *testing.T) {
for _, tt := range postTests {
t.Run(tt.name, func(t *testing.T) {
a, err := NewAzureDevOps(context.TODO(), "kustomization/gitops-system/0c9c2e41", "https://example.com/foo/bar/_git/baz", "foo", nil, "", "", "", "", nil, nil)
a, err := NewAzureDevOps("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com/foo/bar/_git/baz", "foo", nil)
fakeClient := &fakeDevOpsClient{}
a.Client = fakeClient
assert.Nil(t, err)

View File

@ -17,60 +17,37 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
eventhub "github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"github.com/Azure/azure-amqp-common-go/v4/auth"
eventhub "github.com/Azure/azure-event-hubs-go/v3"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/auth/azure"
"github.com/fluxcd/pkg/cache"
)
// AzureEventHub holds the eventhub client
type AzureEventHub struct {
ProducerClient *eventhub.ProducerClient
Hub *eventhub.Hub
}
// NewAzureEventHub creates a eventhub client
func NewAzureEventHub(ctx context.Context, endpointURL, token, eventHubNamespace, proxy,
serviceAccountName, providerName, providerNamespace string, tokenClient client.Client,
tokenCache *cache.TokenCache) (*AzureEventHub, error) {
var client *eventhub.ProducerClient
func NewAzureEventHub(endpointURL, token, eventHubNamespace string) (*AzureEventHub, error) {
var hub *eventhub.Hub
var err error
if err := validateAuthOptions(endpointURL, token, serviceAccountName); err != nil {
return nil, fmt.Errorf("invalid authentication options: %v", err)
}
if isSASAuth(endpointURL) {
client, err = newSASHub(endpointURL)
// token should only be defined if JWT is used
if token != "" {
hub, err = newJWTHub(endpointURL, token, eventHubNamespace)
if err != nil {
return nil, fmt.Errorf("failed to create a eventhub using SAS: %w", err)
return nil, fmt.Errorf("failed to create a eventhub using JWT %v", err)
}
} else {
// if token doesn't exist, try to create a new token using managed identity
if token == "" {
token, err = newManagedIdentityToken(ctx, proxy, serviceAccountName, providerName,
providerNamespace, azure.ScopeEventHubs, tokenClient, tokenCache)
if err != nil {
return nil, fmt.Errorf("failed to create a eventhub using managed identity: %w", err)
}
} else {
log.FromContext(ctx).Error(nil, "warning: static JWT authentication is deprecated and will be removed in the future, prefer workload identity: https://fluxcd.io/flux/components/notification/providers/#managed-identity")
}
client, err = newJWTHub(endpointURL, token, eventHubNamespace)
hub, err = newSASHub(endpointURL)
if err != nil {
return nil, fmt.Errorf("failed to create a eventhub using authentication token: %w", err)
return nil, fmt.Errorf("failed to create a eventhub using SAS %v", err)
}
}
return &AzureEventHub{
ProducerClient: client,
Hub: hub,
}, nil
}
@ -86,22 +63,12 @@ func (e *AzureEventHub) Post(ctx context.Context, event eventv1.Event) error {
return fmt.Errorf("unable to marshall event: %w", err)
}
eventBatch, err := e.ProducerClient.NewEventDataBatch(ctx, nil)
if err != nil {
return fmt.Errorf("failed to create event data batch: %w", err)
}
err = eventBatch.AddEventData(&eventhub.EventData{Body: eventBytes}, nil)
if err != nil {
return fmt.Errorf("failed to add event data to batch: %w", err)
}
err = e.ProducerClient.SendEventDataBatch(ctx, eventBatch, nil)
err = e.Hub.Send(ctx, eventhub.NewEvent(eventBytes))
if err != nil {
return fmt.Errorf("failed to send msg: %w", err)
}
err = e.ProducerClient.Close(ctx)
err = e.Hub.Close(ctx)
if err != nil {
return fmt.Errorf("unable to close connection: %w", err)
}
@ -114,24 +81,26 @@ type PureJWT struct {
}
// NewJWTProvider create a pureJWT method
func NewJWTProvider(jwt string) azcore.TokenCredential {
func NewJWTProvider(jwt string) *PureJWT {
return &PureJWT{
jwt: jwt,
}
}
// GetToken uses a JWT token, we assume that we will get new tokens when needed, thus no Expiry defined
func (j *PureJWT) GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) {
return azcore.AccessToken{
Token: j.jwt,
func (j *PureJWT) GetToken(uri string) (*auth.Token, error) {
return &auth.Token{
TokenType: auth.CBSTokenTypeJWT,
Token: j.jwt,
Expiry: "",
}, nil
}
// newJWTHub used when address is a JWT token
func newJWTHub(eventhubName, token, eventHubNamespace string) (*eventhub.ProducerClient, error) {
func newJWTHub(eventhubName, token, eventHubNamespace string) (*eventhub.Hub, error) {
provider := NewJWTProvider(token)
fullyQualifiedNamespace := ensureFullyQualifiedNamespace(eventHubNamespace)
hub, err := eventhub.NewProducerClient(fullyQualifiedNamespace, eventhubName, provider, nil)
hub, err := eventhub.NewHub(eventHubNamespace, eventhubName, provider)
if err != nil {
return nil, err
}
@ -139,66 +108,11 @@ func newJWTHub(eventhubName, token, eventHubNamespace string) (*eventhub.Produce
}
// newSASHub used when address is a SAS ConnectionString
func newSASHub(address string) (*eventhub.ProducerClient, error) {
client, err := eventhub.NewProducerClientFromConnectionString(address, "", nil)
func newSASHub(address string) (*eventhub.Hub, error) {
hub, err := eventhub.NewHubFromConnectionString(address)
if err != nil {
return nil, err
}
return client, nil
}
// validateAuthOptions checks if the authentication options are valid
func validateAuthOptions(endpointURL, token, serviceAccountName string) error {
if endpointURL == "" {
return fmt.Errorf("endpoint URL cannot be empty")
}
if isSASAuth(endpointURL) {
if err := validateSASAuth(token, serviceAccountName); err != nil {
return err
}
} else if serviceAccountName != "" && token != "" {
return fmt.Errorf("serviceAccountName and jwt token authentication cannot be set at the same time")
}
return nil
}
// isSASAuth checks if the endpoint URL contains SAS authentication parameters
func isSASAuth(endpointURL string) bool {
return strings.Contains(endpointURL, "SharedAccessKey")
}
// validateSASAuth checks if SAS authentication is used correctly
func validateSASAuth(token, serviceAccountName string) error {
if serviceAccountName != "" {
return fmt.Errorf("serviceAccountName and SAS authentication cannot be set at the same time")
}
if token != "" {
return fmt.Errorf("jwt token and SAS authentication cannot be set at the same time")
}
return nil
}
// getEventHubSuffixFromAuthorityHost maps AZURE_AUTHORITY_HOST to the correct suffix
func getEventHubSuffixFromAuthorityHost() string {
authorityHost := os.Getenv("AZURE_AUTHORITY_HOST")
switch {
case strings.Contains(authorityHost, "chinacloudapi.cn"):
return ".servicebus.chinacloudapi.cn"
case strings.Contains(authorityHost, "microsoftonline.us"):
return ".servicebus.usgovcloudapi.net"
default:
return ".servicebus.windows.net"
}
}
// ensureFullyQualifiedNamespace appends suffix if not already present
func ensureFullyQualifiedNamespace(namespace string) string {
if strings.Contains(namespace, ".servicebus.") {
return namespace // already fully qualified
}
return namespace + getEventHubSuffixFromAuthorityHost()
return hub, nil
}

View File

@ -1,103 +0,0 @@
/*
Copyright 2021 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 notifier
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewAzureEventHub(t *testing.T) {
tests := []struct {
name string
endpointURL string
token string
eventHubNamespace string
serviceAccountName string
err error
}{
{
name: "JWT Authentication",
endpointURL: "azure-nc-eventhub",
token: "jwt-token",
eventHubNamespace: "namespace",
},
{
name: "SAS Authentication",
endpointURL: "Endpoint=sb://example.com/;SharedAccessKeyName=keyName;SharedAccessKey=key;EntityPath=eventhub",
token: "",
eventHubNamespace: "namespace",
},
{
name: "SAS Authentication without entity path",
endpointURL: "Endpoint=sb://example.com/;SharedAccessKeyName=keyName;SharedAccessKey=key",
token: "",
eventHubNamespace: "namespace",
err: errors.New("failed to create a eventhub using SAS: connection string does not contain an EntityPath. eventHub cannot be an empty string"),
},
{
name: "Default Azure Credential",
endpointURL: "azure-nc-eventhub",
token: "",
eventHubNamespace: "namespace",
err: errors.New("failed to create a eventhub using managed identity: failed to get token: failed to create provider access token for the controller: ManagedIdentityCredential: failed to authenticate a system assigned identity. The endpoint responded with {\"error\":\"invalid_request\",\"error_description\":\"Identity not found\"}"),
},
{
name: "SAS auth with serviceAccountName set",
endpointURL: "Endpoint=sb://example.com/;SharedAccessKeyName=keyName;SharedAccessKey=key;EntityPath=eventhub",
token: "",
serviceAccountName: "test-service-account",
eventHubNamespace: "namespace",
err: errors.New("invalid authentication options: serviceAccountName and SAS authentication cannot be set at the same time"),
},
{
name: "SAS auth with token set",
endpointURL: "Endpoint=sb://example.com/;SharedAccessKeyName=keyName;SharedAccessKey=key;EntityPath=eventhub",
token: "test-token",
eventHubNamespace: "namespace",
err: errors.New("invalid authentication options: jwt token and SAS authentication cannot be set at the same time"),
},
{
name: "token auth with serviceAccountName set",
endpointURL: "azure-nc-eventhub",
token: "test-token",
serviceAccountName: "test-service-account",
eventHubNamespace: "namespace",
err: errors.New("invalid authentication options: serviceAccountName and jwt token authentication cannot be set at the same time"),
},
{
name: "empty endpoint URL",
endpointURL: "",
token: "test-token",
eventHubNamespace: "namespace",
err: errors.New("invalid authentication options: endpoint URL cannot be empty"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewAzureEventHub(context.TODO(), tt.endpointURL, tt.token, tt.eventHubNamespace, "", tt.serviceAccountName, "", "", nil, nil)
if tt.err != nil {
assert.Error(t, err)
assert.ErrorContains(t, err, tt.err.Error())
} else {
assert.NoError(t, err)
assert.NotNil(t, client)
}
})
}
}

View File

@ -1,69 +0,0 @@
/*
Copyright 2025 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 notifier
import (
"context"
"fmt"
"net/url"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/auth"
"github.com/fluxcd/pkg/auth/azure"
"github.com/fluxcd/pkg/cache"
"github.com/fluxcd/notification-controller/api/v1beta3"
)
// newManagedIdentityToken is used to attempt credential-free authentication.
func newManagedIdentityToken(ctx context.Context, proxy, serviceAccountName, providerName, providerNamespace, scope string, tokenClient client.Client, tokenCache *cache.TokenCache) (string, error) {
opts := []auth.Option{
auth.WithScopes(scope),
auth.WithClient(tokenClient),
auth.WithServiceAccountNamespace(providerNamespace),
}
if proxy != "" {
proxyURL, err := url.Parse(proxy)
if err != nil {
return "", fmt.Errorf("error parsing proxy URL: %w", err)
}
opts = append(opts, auth.WithProxyURL(*proxyURL))
}
if serviceAccountName != "" {
opts = append(opts, auth.WithServiceAccountName(serviceAccountName))
}
if tokenCache != nil {
involvedObject := cache.InvolvedObject{
Kind: v1beta3.ProviderKind,
Name: providerName,
Namespace: providerNamespace,
Operation: OperationPost,
}
opts = append(opts, auth.WithCache(*tokenCache, involvedObject))
}
token, err := auth.GetAccessToken(ctx, azure.Provider{}, opts...)
if err != nil {
return "", fmt.Errorf("failed to get token: %w", err)
}
return token.(*azure.Token).Token, nil
}

View File

@ -19,6 +19,7 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
@ -33,23 +34,18 @@ import (
// Bitbucket is a Bitbucket Server notifier.
type Bitbucket struct {
Owner string
Repo string
CommitStatus string
Client *bitbucket.Client
Owner string
Repo string
ProviderUID string
Client *bitbucket.Client
}
// NewBitbucket creates and returns a new Bitbucket notifier.
func NewBitbucket(commitStatus string, addr string, token string, tlsConfig *tls.Config) (*Bitbucket, error) {
func NewBitbucket(providerUID string, addr string, token string, certPool *x509.CertPool) (*Bitbucket, error) {
if len(token) == 0 {
return nil, errors.New("bitbucket token cannot be empty")
}
// this should never happen
if commitStatus == "" {
return nil, errors.New("commit status cannot be empty")
}
_, id, err := parseGitAddress(addr)
if err != nil {
return nil, err
@ -70,19 +66,21 @@ func NewBitbucket(commitStatus string, addr string, token string, tlsConfig *tls
repo := comp[1]
client := bitbucket.NewBasicAuth(username, password)
if tlsConfig != nil {
if certPool != nil {
tr := &http.Transport{
TLSClientConfig: tlsConfig,
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
}
hc := &http.Client{Transport: tr}
client.HttpClient = hc
}
return &Bitbucket{
Owner: owner,
Repo: repo,
CommitStatus: commitStatus,
Client: client,
Owner: owner,
Repo: repo,
ProviderUID: providerUID,
Client: client,
}, nil
}
@ -93,7 +91,7 @@ func (b Bitbucket) Post(ctx context.Context, event eventv1.Event) error {
return nil
}
revString, ok := event.GetRevision()
revString, ok := event.Metadata[eventv1.MetaRevisionKey]
if !ok {
return errors.New("missing revision metadata")
}
@ -107,7 +105,7 @@ func (b Bitbucket) Post(ctx context.Context, event eventv1.Event) error {
}
name, desc := formatNameAndDescription(event)
id := b.CommitStatus
id := generateCommitStatusID(b.ProviderUID, event)
// key has a limitation of 40 characters in bitbucket api
key := sha1String(id)

View File

@ -18,7 +18,6 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
@ -32,10 +31,10 @@ import (
)
func Fuzz_Bitbucket(f *testing.F) {
f.Add("kustomization/gitops-system/0c9c2e41", "user:pass", "org/repo", "revision/dsa123a", "info", []byte{}, []byte(`{"state":"SUCCESSFUL","description":"","key":"","name":"","url":""}`))
f.Add("kustomization/gitops-system/0c9c2e41", "user:pass", "org/repo", "revision/dsa123a", "error", []byte{}, []byte(`{}`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "user:pass", "org/repo", "revision/dsa123a", "info", []byte{}, []byte(`{"state":"SUCCESSFUL","description":"","key":"","name":"","url":""}`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "user:pass", "org/repo", "revision/dsa123a", "error", []byte{}, []byte(`{}`))
f.Fuzz(func(t *testing.T, commitStatus, token, urlSuffix, revision, severity string, seed, response []byte) {
f.Fuzz(func(t *testing.T, uuid, token, urlSuffix, revision, severity string, seed, response []byte) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(io.Discard, r.Body)
w.Write(response)
@ -46,8 +45,7 @@ func Fuzz_Bitbucket(f *testing.F) {
var cert x509.CertPool
_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
tlsConfig := &tls.Config{RootCAs: &cert}
bitbucket, err := NewBitbucket(commitStatus, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, tlsConfig)
bitbucket, err := NewBitbucket(uuid, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert)
if err != nil {
return
}

View File

@ -23,24 +23,18 @@ import (
)
func TestNewBitbucketBasic(t *testing.T) {
b, err := NewBitbucket("kustomization/gitops-system/0c9c2e41", "https://bitbucket.org/foo/bar", "foo:bar", nil)
b, err := NewBitbucket("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://bitbucket.org/foo/bar", "foo:bar", nil)
assert.Nil(t, err)
assert.Equal(t, b.Owner, "foo")
assert.Equal(t, b.Repo, "bar")
assert.Equal(t, b.CommitStatus, "kustomization/gitops-system/0c9c2e41")
}
func TestNewBitbucketEmptyCommitStatus(t *testing.T) {
_, err := NewBitbucket("", "https://bitbucket.org/foo/bar", "foo:bar", nil)
assert.NotNil(t, err)
}
func TestNewBitbucketInvalidUrl(t *testing.T) {
_, err := NewBitbucket("kustomization/gitops-system/0c9c2e41", "https://bitbucket.org/foo/bar/baz", "foo:bar", nil)
_, err := NewBitbucket("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://bitbucket.org/foo/bar/baz", "foo:bar", nil)
assert.NotNil(t, err)
}
func TestNewBitbucketInvalidToken(t *testing.T) {
_, err := NewBitbucket("kustomization/gitops-system/0c9c2e41", "https://bitbucket.org/foo/bar", "bar", nil)
_, err := NewBitbucket("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://bitbucket.org/foo/bar", "bar", nil)
assert.NotNil(t, err)
}

View File

@ -20,6 +20,7 @@ import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
@ -36,9 +37,11 @@ import (
// BitbucketServer is a notifier for BitBucket Server and Data Center.
type BitbucketServer struct {
CommitStatus string
Url *url.URL
ProjectKey string
RepositorySlug string
ProviderUID string
ProviderAddress string
Host string
Username string
Password string
Token string
@ -46,10 +49,8 @@ type BitbucketServer struct {
}
const (
bbServerEndPointCommitsTmpl = "%[1]s/rest/api/latest/projects/%[2]s/repos/%[3]s/commits"
bbServerEndPointBuildsTmpl = "%[1]s/builds"
bbServerEndPointTmpl = "/rest/api/latest/projects/%[1]s/repos/%[2]s/commits/%[3]s/builds"
bbServerGetBuildStatusQueryString = "key"
bbServerSourceCodeMgmtString = "/scm/"
)
type bbServerBuildStatus struct {
@ -80,21 +81,25 @@ type bbServerBuildStatusSetRequest struct {
}
// NewBitbucketServer creates and returns a new BitbucketServer notifier.
func NewBitbucketServer(commitStatus string, addr string, token string, tlsConfig *tls.Config, username string, password string) (*BitbucketServer, error) {
url, err := parseBitbucketServerGitAddress(addr)
func NewBitbucketServer(providerUID string, addr string, token string, certPool *x509.CertPool, username string, password string) (*BitbucketServer, error) {
hst, id, err := parseBitbucketServerGitAddress(addr)
if err != nil {
return nil, err
}
// this should never happen
if commitStatus == "" {
return nil, errors.New("commit status cannot be empty")
comp := strings.Split(id, "/")
if len(comp) != 2 {
return nil, fmt.Errorf("invalid repository id %q", id)
}
projectkey := comp[0]
reposlug := comp[1]
httpClient := retryablehttp.NewClient()
if tlsConfig != nil {
if certPool != nil {
httpClient.HTTPClient.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
}
}
@ -109,8 +114,10 @@ func NewBitbucketServer(commitStatus string, addr string, token string, tlsConfi
}
return &BitbucketServer{
CommitStatus: commitStatus,
Url: url,
ProjectKey: projectkey,
RepositorySlug: reposlug,
ProviderUID: providerUID,
Host: hst,
ProviderAddress: addr,
Token: token,
Username: username,
@ -125,7 +132,7 @@ func (b BitbucketServer) Post(ctx context.Context, event eventv1.Event) error {
if event.HasReason(meta.ProgressingReason) {
return nil
}
revString, ok := event.GetRevision()
revString, ok := event.Metadata[eventv1.MetaRevisionKey]
if !ok {
return errors.New("missing revision metadata")
}
@ -140,18 +147,18 @@ func (b BitbucketServer) Post(ctx context.Context, event eventv1.Event) error {
name, desc := formatNameAndDescription(event)
name = name + " [" + desc + "]" //Bitbucket server displays this data on browser. Thus adding description here.
id := b.CommitStatus
id := generateCommitStatusID(b.ProviderUID, event)
// key has a limitation of 40 characters in bitbucket api
key := sha1String(id)
u := b.Url.JoinPath(b.createBuildPath(rev)).String()
dupe, err := b.duplicateBitbucketServerStatus(ctx, state, name, desc, key, u)
u := b.Host + b.createApiPath(rev)
dupe, err := b.duplicateBitbucketServerStatus(ctx, rev, state, name, desc, id, key, u)
if err != nil {
return fmt.Errorf("could not get existing commit status: %w", err)
}
if !dupe {
_, err = b.postBuildStatus(ctx, state, name, desc, key, u)
_, err = b.postBuildStatus(ctx, rev, state, name, desc, id, key, u)
if err != nil {
return fmt.Errorf("could not post build status: %w", err)
}
@ -171,9 +178,9 @@ func (b BitbucketServer) state(severity string) (string, error) {
}
}
func (b BitbucketServer) duplicateBitbucketServerStatus(ctx context.Context, state, name, desc, key, u string) (bool, error) {
func (b BitbucketServer) duplicateBitbucketServerStatus(ctx context.Context, rev, state, name, desc, id, key, u string) (bool, error) {
// Prepare request object
req, err := b.prepareCommonRequest(ctx, u, nil, http.MethodGet)
req, err := b.prepareCommonRequest(ctx, u, nil, http.MethodGet, key, rev)
if err != nil {
return false, fmt.Errorf("could not check duplicate commit status: %w", err)
}
@ -185,10 +192,10 @@ func (b BitbucketServer) duplicateBitbucketServerStatus(ctx context.Context, sta
// Make a GET call
d, err := b.Client.Do(req)
if err != nil {
if err != nil && d.StatusCode != http.StatusNotFound {
return false, fmt.Errorf("failed api call to check duplicate commit status: %w", err)
}
if d != nil && isError(d) && d.StatusCode != http.StatusNotFound {
if isError(d) && d.StatusCode != http.StatusNotFound {
defer d.Body.Close()
return false, fmt.Errorf("failed api call to check duplicate commit status: %d - %s", d.StatusCode, http.StatusText(d.StatusCode))
}
@ -212,7 +219,7 @@ func (b BitbucketServer) duplicateBitbucketServerStatus(ctx context.Context, sta
return false, nil
}
func (b BitbucketServer) postBuildStatus(ctx context.Context, state, name, desc, key, url string) (*http.Response, error) {
func (b BitbucketServer) postBuildStatus(ctx context.Context, rev, state, name, desc, id, key, url string) (*http.Response, error) {
//Prepare json body
j := &bbServerBuildStatusSetRequest{
Key: key,
@ -228,7 +235,7 @@ func (b BitbucketServer) postBuildStatus(ctx context.Context, state, name, desc,
}
//Prepare request
req, err := b.prepareCommonRequest(ctx, url, p, http.MethodPost)
req, err := b.prepareCommonRequest(ctx, url, p, http.MethodPost, key, rev)
if err != nil {
return nil, fmt.Errorf("failed preparing request for post build commit status: %w", err)
}
@ -250,46 +257,21 @@ func (b BitbucketServer) postBuildStatus(ctx context.Context, state, name, desc,
return resp, nil
}
func (b BitbucketServer) createBuildPath(rev string) string {
return fmt.Sprintf(bbServerEndPointBuildsTmpl, rev)
func (b BitbucketServer) createApiPath(rev string) string {
return fmt.Sprintf(bbServerEndPointTmpl, b.ProjectKey, b.RepositorySlug, rev)
}
func parseBitbucketServerGitAddress(s string) (*url.URL, error) {
u, err := url.Parse(s)
func parseBitbucketServerGitAddress(s string) (string, string, error) {
host, id, err := parseGitAddress(s)
if err != nil {
return nil, fmt.Errorf("could not parse git address: %w", err)
return "", "", fmt.Errorf("could not parse git address: %w", err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, fmt.Errorf("could not parse git address: unsupported scheme type in address: %s. Must be http or https", u.Scheme)
}
idWithContext := strings.TrimSuffix(u.Path, ".git")
// /scm/ is always part of http/https clone urls : https://community.atlassian.com/t5/Bitbucket-questions/remote-url-in-Bitbucket-server-what-does-scm-represent-is-it/qaq-p/2060987
lastIndex := strings.LastIndex(idWithContext, bbServerSourceCodeMgmtString)
if lastIndex < 0 {
return nil, fmt.Errorf("could not parse git address: supplied provider address is not http(s) git clone url")
}
// Handle context scenarios --> https://confluence.atlassian.com/bitbucketserver/change-bitbucket-s-context-path-776640153.html
cntxtPath := idWithContext[:lastIndex] // Context path is anything that comes before last /scm/
id := idWithContext[lastIndex+len(bbServerSourceCodeMgmtString):] // Remove last `/scm/` from id as it is not used in API calls
comp := strings.Split(id, "/")
if len(comp) != 2 {
return nil, fmt.Errorf("could not parse git address: invalid repository id %q", id)
}
projectkey := comp[0]
reposlug := comp[1]
// Update the path till commits endpoint. The final builds endpoint would be added in Post function.
u.Path = fmt.Sprintf(bbServerEndPointCommitsTmpl, cntxtPath, projectkey, reposlug)
return u, nil
//Remove "scm/" --> https://community.atlassian.com/t5/Bitbucket-questions/remote-url-in-Bitbucket-server-what-does-scm-represent-is-it/qaq-p/2060987
id = strings.TrimPrefix(id, "scm/")
return host, id, nil
}
func (b BitbucketServer) prepareCommonRequest(ctx context.Context, path string, body io.Reader, method string) (*retryablehttp.Request, error) {
func (b BitbucketServer) prepareCommonRequest(ctx context.Context, path string, body io.Reader, method string, key, rev string) (*retryablehttp.Request, error) {
req, err := retryablehttp.NewRequestWithContext(ctx, method, path, body)
if err != nil {
return nil, fmt.Errorf("could not prepare request: %w", err)

View File

@ -20,7 +20,6 @@ import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"testing"
@ -34,91 +33,33 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestNewBitbucketServerBasicNoContext(t *testing.T) {
b, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
func TestNewBitbucketServerBasic(t *testing.T) {
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
assert.Equal(t, b.Username, "dummyuser")
assert.Equal(t, b.Password, "testpassword")
assert.Equal(t, b.Url.Scheme, "https")
assert.Equal(t, b.Url.Host, "example.com:7990")
}
func TestNewBitbucketServerBasicWithContext(t *testing.T) {
b, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/context/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
assert.Equal(t, b.Username, "dummyuser")
assert.Equal(t, b.Password, "testpassword")
assert.Equal(t, b.Url.Scheme, "https")
assert.Equal(t, b.Url.Host, "example.com:7990")
}
func TestBitbucketServerApiPathNoContext(t *testing.T) {
b, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
u := b.Url.JoinPath(b.createBuildPath("00151b98e303e19610378e6f1c49e31e5e80cd3b")).String()
assert.Equal(t, u, "https://example.com:7990/rest/api/latest/projects/projectfoo/repos/repobar/commits/00151b98e303e19610378e6f1c49e31e5e80cd3b/builds")
}
func TestBitbucketServerApiPathOneWordContext(t *testing.T) {
b, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/context1/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
u := b.Url.JoinPath(b.createBuildPath("00151b98e303e19610378e6f1c49e31e5e80cd3b")).String()
assert.Equal(t, u, "https://example.com:7990/context1/rest/api/latest/projects/projectfoo/repos/repobar/commits/00151b98e303e19610378e6f1c49e31e5e80cd3b/builds")
}
func TestBitbucketServerApiPathMultipleWordContext(t *testing.T) {
b, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/context1/context2/context3/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
u := b.Url.JoinPath(b.createBuildPath("00151b98e303e19610378e6f1c49e31e5e80cd3b")).String()
assert.Equal(t, u, "https://example.com:7990/context1/context2/context3/rest/api/latest/projects/projectfoo/repos/repobar/commits/00151b98e303e19610378e6f1c49e31e5e80cd3b/builds")
}
func TestBitbucketServerApiPathOneWordScmInContext(t *testing.T) {
b, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/scm/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
u := b.Url.JoinPath(b.createBuildPath("00151b98e303e19610378e6f1c49e31e5e80cd3b")).String()
assert.Equal(t, u, "https://example.com:7990/scm/rest/api/latest/projects/projectfoo/repos/repobar/commits/00151b98e303e19610378e6f1c49e31e5e80cd3b/builds")
}
func TestBitbucketServerApiPathMultipleWordScmInContext(t *testing.T) {
b, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/scm/context2/scm/scm/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.Nil(t, err)
u := b.Url.JoinPath(b.createBuildPath("00151b98e303e19610378e6f1c49e31e5e80cd3b")).String()
assert.Equal(t, u, "https://example.com:7990/scm/context2/scm/rest/api/latest/projects/projectfoo/repos/repobar/commits/00151b98e303e19610378e6f1c49e31e5e80cd3b/builds")
}
func TestBitbucketServerApiPathScmAlreadyRemovedInInput(t *testing.T) {
_, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/context1/context2/context3/projectfoo/repobar.git", "", nil, "dummyuser", "testpassword")
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "could not parse git address: supplied provider address is not http(s) git clone url")
}
func TestBitbucketServerSshAddress(t *testing.T) {
_, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "ssh://git@mybitbucket:2222/ap/fluxcd-sandbox.git", "", nil, "", "")
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "could not parse git address: unsupported scheme type in address: ssh. Must be http or https")
}
func TestNewBitbucketServerToken(t *testing.T) {
b, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
assert.Nil(t, err)
assert.Equal(t, b.Token, "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP")
}
func TestNewBitbucketServerInvalidCreds(t *testing.T) {
_, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/scm/projectfoo/repobar.git", "", nil, "", "")
_, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "", nil, "", "")
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "invalid credentials, expected to be one of username/password or API Token")
}
func TestNewBitbucketServerInvalidRepo(t *testing.T) {
_, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/scm/projectfoo/repobar/invalid.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
_, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar/invalid.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "could not parse git address: invalid repository id \"projectfoo/repobar/invalid\"")
assert.Equal(t, err.Error(), "invalid repository id \"projectfoo/repobar/invalid\"")
}
func TestPostBitbucketServerMissingRevision(t *testing.T) {
b, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
assert.Nil(t, err)
//Validate missing revision
@ -129,14 +70,8 @@ func TestPostBitbucketServerMissingRevision(t *testing.T) {
assert.Equal(t, err.Error(), "missing revision metadata")
}
func TestNewBitbucketServerEmptyCommitStatus(t *testing.T) {
_, err := NewBitbucketServer("", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "commit status cannot be empty")
}
func TestPostBitbucketServerBadCommitHash(t *testing.T) {
b, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
assert.Nil(t, err)
//Validate extract commit hash
@ -149,7 +84,7 @@ func TestPostBitbucketServerBadCommitHash(t *testing.T) {
}
func TestPostBitbucketServerBadBitbucketState(t *testing.T) {
b, err := NewBitbucketServer("kustomization/gitops-system/0c9c2e41", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
b, err := NewBitbucketServer("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com:7990/scm/projectfoo/repobar.git", "BBDC-ODIxODYxMzIyNzUyOttorMjO059P2rYTb6EH7mP", nil, "", "")
assert.Nil(t, err)
//Validate conversion to bitbucket state
@ -188,14 +123,13 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
password string
token string
event eventv1.Event
commitStatus string
provideruid string
key string
uriHash string
}{
{
name: "Validate Token Auth ",
token: "goodtoken",
commitStatus: "kustomization/gitops-system/0c9c2e41",
name: "Validate Token Auth ",
token: "goodtoken",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Bearer goodtoken",
"x-atlassian-token": "no-check",
@ -204,30 +138,15 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
event: generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String("kustomization/gitops-system/0c9c2e41"),
uriHash: "5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Event with origin revision",
token: "goodtoken",
commitStatus: "kustomization/gitops-system/0c9c2e41",
headers: map[string]string{
"Authorization": "Bearer goodtoken",
"x-atlassian-token": "no-check",
"x-requested-with": "XMLHttpRequest",
},
event: generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
eventv1.MetaOriginRevisionKey: "main@sha1:e7c17dd8b8384bbc84b7e7385394cb7f48332b2d",
}),
key: sha1String("kustomization/gitops-system/0c9c2e41"),
uriHash: "e7c17dd8b8384bbc84b7e7385394cb7f48332b2d",
},
{
name: "Validate Basic Auth and Post State=Successful",
username: "hello",
password: "password",
commitStatus: "kustomization/gitops-system/0c9c2e41",
name: "Validate Basic Auth and Post State=Successful",
username: "hello",
password: "password",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
@ -236,14 +155,15 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
event: generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String("kustomization/gitops-system/0c9c2e41"),
uriHash: "5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Validate Post State=Failed",
username: "hello",
password: "password",
commitStatus: "kustomization/gitops-system/0c9c2e41",
name: "Validate Post State=Failed",
username: "hello",
password: "password",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
@ -252,8 +172,9 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
event: generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String("kustomization/gitops-system/0c9c2e41"),
uriHash: "5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Fail if bad json response in existing commit status",
@ -261,7 +182,7 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
errorString: "could not get existing commit status: could not unmarshal json response body for duplicate commit status: unexpected end of JSON input",
username: "hello",
password: "password",
commitStatus: "kustomization/gitops-system/0c9c2e41",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
@ -270,8 +191,9 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
event: generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String("kustomization/gitops-system/0c9c2e41"),
uriHash: "5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Fail if status code is non-200 in existing commit status",
@ -279,7 +201,7 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
errorString: "could not get existing commit status: failed api call to check duplicate commit status: 400 - Bad Request",
username: "hello",
password: "password",
commitStatus: "kustomization/gitops-system/0c9c2e41",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
@ -288,8 +210,9 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
event: generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String("kustomization/gitops-system/0c9c2e41"),
uriHash: "5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Bad post- Unauthorized",
@ -297,7 +220,7 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
errorString: "could not post build status: could not post build commit status: 401 - Unauthorized",
username: "hello",
password: "password",
commitStatus: "kustomization/gitops-system/0c9c2e41",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
@ -306,14 +229,15 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
event: generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String("kustomization/gitops-system/0c9c2e41"),
uriHash: "5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("error", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
{
name: "Validate duplicate commit status successful match",
username: "hello",
password: "password",
commitStatus: "kustomization/gitops-system/0c9c2e41",
name: "Validate duplicate commit status successful match",
username: "hello",
password: "password",
provideruid: "0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a",
headers: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("hello"+":"+"password")),
"x-atlassian-token": "no-check",
@ -322,8 +246,9 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
event: generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}),
key: sha1String("kustomization/gitops-system/0c9c2e41"),
uriHash: "5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
key: sha1String(generateCommitStatusID("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", generateTestEventKustomization("info", map[string]string{
eventv1.MetaRevisionKey: "main@sha1:5394cb7f48332b2de7c17dd8b8384bbc84b7e738",
}))),
},
}
@ -337,8 +262,7 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
}
// Validate URI
path := fmt.Sprintf("/rest/api/latest/projects/projectfoo/repos/repobar/commits/%s/builds", tt.uriHash)
require.Equal(t, r.URL.Path, path)
require.Equal(t, r.URL.Path, "/rest/api/latest/projects/projectfoo/repos/repobar/commits/5394cb7f48332b2de7c17dd8b8384bbc84b7e738/builds")
// Validate Get Build Status call
if r.Method == http.MethodGet {
@ -357,7 +281,7 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
jsondata, _ := json.Marshal(&bbServerBuildStatus{
Name: name,
Description: desc,
Key: sha1String(tt.commitStatus),
Key: sha1String(generateCommitStatusID(tt.provideruid, tt.event)),
State: "SUCCESSFUL",
Url: "https://example.com:7990/scm/projectfoo/repobar.git",
})
@ -440,7 +364,7 @@ func TestBitBucketServerPostValidateRequest(t *testing.T) {
}
}))
defer ts.Close()
c, err := NewBitbucketServer(tt.commitStatus, ts.URL+"/scm/projectfoo/repobar.git", tt.token, nil, tt.username, tt.password)
c, err := NewBitbucketServer(tt.provideruid, ts.URL+"/scm/projectfoo/repobar.git", tt.token, nil, tt.username, tt.password)
require.NoError(t, err)
err = c.Post(context.TODO(), tt.event)
if tt.testFailReason == "" {

View File

@ -19,130 +19,91 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"runtime"
"time"
"github.com/hashicorp/go-retryablehttp"
)
type postOptions struct {
proxy string
tlsConfig *tls.Config
requestModifier func(*retryablehttp.Request)
responseValidator func(statusCode int, body []byte) error
}
type requestOptFunc func(*retryablehttp.Request)
type postOption func(*postOptions)
func postMessage(ctx context.Context, address, proxy string, certPool *x509.CertPool, payload interface{}, reqOpts ...requestOptFunc) error {
httpClient := retryablehttp.NewClient()
if certPool != nil {
httpClient.HTTPClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
}
}
func postMessage(ctx context.Context, address string, payload interface{}, opts ...postOption) error {
options := &postOptions{
// Default validateResponse function verifies that the response status code is 200, 202 or 201.
responseValidator: func(statusCode int, body []byte) error {
if statusCode == http.StatusOK ||
statusCode == http.StatusAccepted ||
statusCode == http.StatusCreated {
return nil
if proxy != "" {
proxyURL, err := url.Parse(proxy)
if err != nil {
return fmt.Errorf("unable to parse proxy URL '%s', error: %w", proxy, err)
}
var tlsConfig *tls.Config
if certPool != nil {
tlsConfig = &tls.Config{
RootCAs: certPool,
}
return fmt.Errorf("request failed with status code %d, %s", statusCode, string(body))
},
}
httpClient.HTTPClient.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyURL),
TLSClientConfig: tlsConfig,
DialContext: (&net.Dialer{
Timeout: 15 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
}
}
for _, o := range opts {
o(options)
}
httpClient, err := newHTTPClient(options)
if err != nil {
return err
}
httpClient.HTTPClient.Timeout = 15 * time.Second
httpClient.RetryWaitMin = 2 * time.Second
httpClient.RetryWaitMax = 30 * time.Second
httpClient.RetryMax = 4
httpClient.Logger = nil
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshalling notification payload failed: %w", err)
}
req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodPost, address, data)
req, err := retryablehttp.NewRequest(http.MethodPost, address, data)
if err != nil {
return fmt.Errorf("failed to create a new request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if options.requestModifier != nil {
options.requestModifier(req)
if ctx != nil {
req = req.WithContext(ctx)
}
req.Header.Set("Content-Type", "application/json")
for _, o := range reqOpts {
o(req)
}
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
if err := options.responseValidator(resp.StatusCode, body); err != nil {
return fmt.Errorf("request failed: %w", err)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusCreated {
b, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("unable to read response body, %s", err)
}
return fmt.Errorf("request failed with status code %d, %s", resp.StatusCode, string(b))
}
return nil
}
func withProxy(proxy string) postOption {
return func(opts *postOptions) {
opts.proxy = proxy
}
}
func withTLSConfig(tlsConfig *tls.Config) postOption {
return func(opts *postOptions) {
opts.tlsConfig = tlsConfig
}
}
func withRequestModifier(reqModifier func(*retryablehttp.Request)) postOption {
return func(opts *postOptions) {
opts.requestModifier = reqModifier
}
}
func withResponseValidator(respValidator func(statusCode int, body []byte) error) postOption {
return func(opts *postOptions) {
opts.responseValidator = respValidator
}
}
func newHTTPClient(opts *postOptions) (*retryablehttp.Client, error) {
httpClient := retryablehttp.NewClient()
transport := httpClient.HTTPClient.Transport.(*http.Transport)
if opts.tlsConfig != nil {
transport.TLSClientConfig = opts.tlsConfig
}
if opts.proxy != "" {
proxyURL, err := url.Parse(opts.proxy)
if err != nil {
return nil, fmt.Errorf("unable to parse proxy URL: %w", err)
}
transport.Proxy = http.ProxyURL(proxyURL)
}
// Disable the timeout for the HTTP client,
// as we set the provider timeout on the context.
httpClient.HTTPClient.Timeout = 0
httpClient.RetryWaitMin = 2 * time.Second
httpClient.RetryWaitMax = 30 * time.Second
httpClient.RetryMax = 4
httpClient.Logger = nil
return httpClient, nil
}

View File

@ -18,19 +18,14 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/hashicorp/go-retryablehttp"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -49,21 +44,10 @@ func Test_postMessage(t *testing.T) {
require.Equal(t, "success", payload["status"])
}))
defer ts.Close()
err := postMessage(context.Background(), ts.URL, map[string]string{"status": "success"})
err := postMessage(context.Background(), ts.URL, "", nil, map[string]string{"status": "success"})
require.NoError(t, err)
}
func Test_postMessage_timeout(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
}))
defer ts.Close()
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
err := postMessage(ctx, ts.URL, map[string]string{"status": "success"})
require.Error(t, err, "context deadline exceeded")
}
func Test_postSelfSignedCert(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, err := io.ReadAll(r.Body)
@ -81,43 +65,10 @@ func Test_postSelfSignedCert(t *testing.T) {
require.NoError(t, err)
certpool := x509.NewCertPool()
certpool.AddCert(cert)
tlsConfig := &tls.Config{RootCAs: certpool}
err = postMessage(context.Background(), ts.URL, map[string]string{"status": "success"}, withTLSConfig(tlsConfig))
err = postMessage(context.Background(), ts.URL, "", certpool, map[string]string{"status": "success"})
require.NoError(t, err)
}
func Test_postMessage_requestModifier(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
require.Equal(t, "Bearer token", r.Header.Get("Authorization"))
}))
defer ts.Close()
err := postMessage(context.Background(), ts.URL, map[string]string{"status": "success"}, withRequestModifier(func(req *retryablehttp.Request) {
req.Header.Set("Authorization", "Bearer token")
}))
require.NoError(t, err)
}
func Test_postMessage_responseValidator(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Default response validator determines success, but the custom validator below will determine failure .
w.WriteHeader(http.StatusOK)
w.Write([]byte("error: bad request"))
}))
defer ts.Close()
err := postMessage(context.Background(), ts.URL, map[string]string{"status": "success"})
require.NoError(t, err)
err = postMessage(context.Background(), ts.URL, map[string]string{"status": "success"}, withResponseValidator(func(_ int, body []byte) error {
if strings.HasPrefix(string(body), "error:") {
return errors.New(string(body))
}
return nil
}))
require.ErrorContains(t, err, "request failed: error: bad request")
}
func testEvent() eventv1.Event {
return eventv1.Event{
InvolvedObject: corev1.ObjectReference{

View File

@ -19,6 +19,7 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"net/url"
@ -40,7 +41,7 @@ type DataDog struct {
// url: The DataDog API endpoint to use. Examples: https://api.datadoghq.com, https://api.datadoghq.eu, etc.
// token: The DataDog API key (not the application key).
// headers: A map of extra tags to add to the event
func NewDataDog(address string, proxyUrl string, tlsConfig *tls.Config, token string) (*DataDog, error) {
func NewDataDog(address string, proxyUrl string, certPool *x509.CertPool, token string) (*DataDog, error) {
conf := datadog.NewConfiguration()
if token == "" {
@ -55,7 +56,7 @@ func NewDataDog(address string, proxyUrl string, tlsConfig *tls.Config, token st
conf.Host = baseUrl.Host
conf.Scheme = baseUrl.Scheme
if proxyUrl != "" || tlsConfig != nil {
if proxyUrl != "" || certPool != nil {
transport := &http.Transport{}
if proxyUrl != "" {
@ -67,8 +68,10 @@ func NewDataDog(address string, proxyUrl string, tlsConfig *tls.Config, token st
transport.Proxy = http.ProxyURL(proxy)
}
if tlsConfig != nil {
transport.TLSClientConfig = tlsConfig
if certPool != nil {
transport.TLSClientConfig = &tls.Config{
RootCAs: certPool,
}
}
conf.HTTPClient = &http.Client{

View File

@ -2,7 +2,6 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"io"
"net/http"
@ -34,8 +33,7 @@ func Fuzz_DataDog(f *testing.F) {
var cert x509.CertPool
_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
tlsConfig := &tls.Config{RootCAs: &cert}
dd, err := NewDataDog(ts.URL, "", tlsConfig, apiKey)
dd, err := NewDataDog(ts.URL, "", &cert, apiKey)
if err != nil {
return
}

View File

@ -90,12 +90,8 @@ func (s *Discord) Post(ctx context.Context, event eventv1.Event) error {
payload.Attachments = []SlackAttachment{a}
var opts []postOption
if s.ProxyURL != "" {
opts = append(opts, withProxy(s.ProxyURL))
}
if err := postMessage(ctx, s.URL, payload, opts...); err != nil {
err := postMessage(ctx, s.URL, s.ProxyURL, nil, payload)
if err != nil {
return fmt.Errorf("postMessage failed: %w", err)
}

View File

@ -17,341 +17,114 @@ limitations under the License.
package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/cache"
apiv1 "github.com/fluxcd/notification-controller/api/v1beta3"
)
var (
// notifiers is a map of notifier names to factory functions.
notifiers = notifierMap{
// GenericProvider is the default notifier
apiv1.GenericProvider: genericNotifierFunc,
apiv1.GenericHMACProvider: genericHMACNotifierFunc,
apiv1.SlackProvider: slackNotifierFunc,
apiv1.DiscordProvider: discordNotifierFunc,
apiv1.RocketProvider: rocketNotifierFunc,
apiv1.MSTeamsProvider: msteamsNotifierFunc,
apiv1.GoogleChatProvider: googleChatNotifierFunc,
apiv1.GooglePubSubProvider: googlePubSubNotifierFunc,
apiv1.WebexProvider: webexNotifierFunc,
apiv1.SentryProvider: sentryNotifierFunc,
apiv1.AzureEventHubProvider: azureEventHubNotifierFunc,
apiv1.TelegramProvider: telegramNotifierFunc,
apiv1.LarkProvider: larkNotifierFunc,
apiv1.Matrix: matrixNotifierFunc,
apiv1.OpsgenieProvider: opsgenieNotifierFunc,
apiv1.AlertManagerProvider: alertmanagerNotifierFunc,
apiv1.GrafanaProvider: grafanaNotifierFunc,
apiv1.PagerDutyProvider: pagerDutyNotifierFunc,
apiv1.DataDogProvider: dataDogNotifierFunc,
apiv1.NATSProvider: natsNotifierFunc,
apiv1.GitHubProvider: gitHubNotifierFunc,
apiv1.GitHubDispatchProvider: gitHubDispatchNotifierFunc,
apiv1.GitLabProvider: gitLabNotifierFunc,
apiv1.GiteaProvider: giteaNotifierFunc,
apiv1.BitbucketServerProvider: bitbucketServerNotifierFunc,
apiv1.BitbucketProvider: bitbucketNotifierFunc,
apiv1.AzureDevOpsProvider: azureDevOpsNotifierFunc,
}
)
// notifierMap is a map of provider names to notifier factory functions
type notifierMap map[string]factoryFunc
// factoryFunc is a factory function that creates a new notifier
type factoryFunc func(opts notifierOptions) (Interface, error)
type notifierOptions struct {
Context context.Context
URL string
ProxyURL string
Username string
Channel string
Token string
Headers map[string]string
// CertPool is kept for Git platform providers (GitHub, GitLab, etc.) that use third-party SDKs.
// TODO: Remove this field once all notifiers support client certificate authentication via TLSConfig.
CertPool *x509.CertPool
TLSConfig *tls.Config
Password string
CommitStatus string
ProviderName string
ProviderNamespace string
SecretData map[string][]byte
ServiceAccountName string
TokenCache *cache.TokenCache
TokenClient client.Client
}
type Factory struct {
notifierOptions
URL string
ProxyURL string
Username string
Channel string
Token string
Headers map[string]string
CertPool *x509.CertPool
Password string
ProviderUID string
}
// Option represents a functional option for configuring a notifier.
type Option func(*notifierOptions)
// WithProxyURL sets the proxy URL for the notifier.
func WithProxyURL(url string) Option {
return func(o *notifierOptions) {
o.ProxyURL = url
}
}
// WithUsername sets the username for the notifier.
func WithUsername(username string) Option {
return func(o *notifierOptions) {
o.Username = username
}
}
// WithChannel sets the channel for the notifier.
func WithChannel(channel string) Option {
return func(o *notifierOptions) {
o.Channel = channel
}
}
// WithToken sets the token for the notifier.
func WithToken(token string) Option {
return func(o *notifierOptions) {
o.Token = token
}
}
// WithHeaders sets the headers for the notifier.
func WithHeaders(headers map[string]string) Option {
return func(o *notifierOptions) {
o.Headers = headers
}
}
// WithCertPool sets the certificate pool for the notifier.
func WithCertPool(certPool *x509.CertPool) Option {
return func(o *notifierOptions) {
o.CertPool = certPool
}
}
// WithTLSConfig sets the TLS configuration for the notifier.
func WithTLSConfig(tlsConfig *tls.Config) Option {
return func(o *notifierOptions) {
o.TLSConfig = tlsConfig
}
}
// WithPassword sets the password for the notifier.
func WithPassword(password string) Option {
return func(o *notifierOptions) {
o.Password = password
}
}
// WithCommitStatus sets the custom commit status for the notifier.
func WithCommitStatus(commitStatus string) Option {
return func(o *notifierOptions) {
o.CommitStatus = commitStatus
}
}
// WithProviderName sets the provider name for the notifier.
func WithProviderName(name string) Option {
return func(o *notifierOptions) {
o.ProviderName = name
}
}
// WithProviderNamespace sets the provider namespace for the notifier.
func WithProviderNamespace(namespace string) Option {
return func(o *notifierOptions) {
o.ProviderNamespace = namespace
}
}
// WithSecretData sets the secret data for the notifier.
func WithSecretData(data map[string][]byte) Option {
return func(o *notifierOptions) {
o.SecretData = data
}
}
// WithTokenCache sets the token cache for the notifier.
func WithTokenCache(cache *cache.TokenCache) Option {
return func(o *notifierOptions) {
o.TokenCache = cache
}
}
// WithTokenClient sets the token client for the notifier.
func WithTokenClient(kubeClient client.Client) Option {
return func(o *notifierOptions) {
o.TokenClient = kubeClient
}
}
// WithServiceAccount sets the service account for the notifier.
func WithServiceAccount(serviceAccountName string) Option {
return func(o *notifierOptions) {
o.ServiceAccountName = serviceAccountName
}
}
// WithURL sets the webhook URL for the notifier.
func WithURL(url string) Option {
return func(o *notifierOptions) {
o.URL = url
}
}
// NewFactory creates a new notifier factory with optional configurations.
func NewFactory(ctx context.Context, opts ...Option) *Factory {
options := notifierOptions{
Context: ctx,
}
for _, opt := range opts {
opt(&options)
}
func NewFactory(url string,
proxy string,
username string,
channel string,
token string,
headers map[string]string,
certPool *x509.CertPool,
password string,
providerUID string) *Factory {
return &Factory{
notifierOptions: options,
URL: url,
ProxyURL: proxy,
Channel: channel,
Username: username,
Token: token,
Headers: headers,
CertPool: certPool,
Password: password,
ProviderUID: providerUID,
}
}
func (f Factory) Notifier(provider string) (Interface, error) {
notifier, ok := notifiers[provider]
if !ok {
return nil, fmt.Errorf("provider %s not supported", provider)
if f.URL == "" {
return &NopNotifier{}, nil
}
return notifier(f.notifierOptions)
}
func genericNotifierFunc(opts notifierOptions) (Interface, error) {
return NewForwarder(opts.URL, opts.ProxyURL, opts.Headers, opts.TLSConfig, nil)
}
func genericHMACNotifierFunc(opts notifierOptions) (Interface, error) {
return NewForwarder(opts.URL, opts.ProxyURL, opts.Headers, opts.TLSConfig, []byte(opts.Token))
}
func slackNotifierFunc(opts notifierOptions) (Interface, error) {
return NewSlack(opts.URL, opts.ProxyURL, opts.Token, opts.TLSConfig, opts.Username, opts.Channel)
}
func discordNotifierFunc(opts notifierOptions) (Interface, error) {
return NewDiscord(opts.URL, opts.ProxyURL, opts.Username, opts.Channel)
}
func rocketNotifierFunc(opts notifierOptions) (Interface, error) {
return NewRocket(opts.URL, opts.ProxyURL, opts.TLSConfig, opts.Username, opts.Channel)
}
func msteamsNotifierFunc(opts notifierOptions) (Interface, error) {
return NewMSTeams(opts.URL, opts.ProxyURL, opts.TLSConfig)
}
func googleChatNotifierFunc(opts notifierOptions) (Interface, error) {
return NewGoogleChat(opts.URL, opts.ProxyURL)
}
func googlePubSubNotifierFunc(opts notifierOptions) (Interface, error) {
return NewGooglePubSub(&opts)
}
func webexNotifierFunc(opts notifierOptions) (Interface, error) {
return NewWebex(opts.URL, opts.ProxyURL, opts.TLSConfig, opts.Channel, opts.Token)
}
func sentryNotifierFunc(opts notifierOptions) (Interface, error) {
return NewSentry(opts.TLSConfig, opts.URL, opts.Channel)
}
func azureEventHubNotifierFunc(opts notifierOptions) (Interface, error) {
return NewAzureEventHub(opts.Context, opts.URL, opts.Token, opts.Channel, opts.ProxyURL, opts.ServiceAccountName, opts.ProviderName, opts.ProviderNamespace, opts.TokenClient, opts.TokenCache)
}
func telegramNotifierFunc(opts notifierOptions) (Interface, error) {
return NewTelegram(opts.ProxyURL, opts.Channel, opts.Token)
}
func larkNotifierFunc(opts notifierOptions) (Interface, error) {
return NewLark(opts.URL)
}
func matrixNotifierFunc(opts notifierOptions) (Interface, error) {
return NewMatrix(opts.URL, opts.Token, opts.Channel, opts.TLSConfig)
}
func opsgenieNotifierFunc(opts notifierOptions) (Interface, error) {
return NewOpsgenie(opts.URL, opts.ProxyURL, opts.TLSConfig, opts.Token)
}
func alertmanagerNotifierFunc(opts notifierOptions) (Interface, error) {
return NewAlertmanager(opts.URL, opts.ProxyURL, opts.TLSConfig, opts.Token, opts.Username, opts.Password)
}
func grafanaNotifierFunc(opts notifierOptions) (Interface, error) {
return NewGrafana(opts.URL, opts.ProxyURL, opts.Token, opts.TLSConfig, opts.Username, opts.Password)
}
func pagerDutyNotifierFunc(opts notifierOptions) (Interface, error) {
return NewPagerDuty(opts.URL, opts.ProxyURL, opts.TLSConfig, opts.Channel)
}
func dataDogNotifierFunc(opts notifierOptions) (Interface, error) {
return NewDataDog(opts.URL, opts.ProxyURL, opts.TLSConfig, opts.Token)
}
func natsNotifierFunc(opts notifierOptions) (Interface, error) {
return NewNATS(opts.URL, opts.Channel, opts.Username, opts.Password)
}
func gitHubNotifierFunc(opts notifierOptions) (Interface, error) {
if opts.Token == "" && opts.Password != "" {
opts.Token = opts.Password
var n Interface
var err error
switch provider {
case apiv1.GenericProvider:
n, err = NewForwarder(f.URL, f.ProxyURL, f.Headers, f.CertPool, nil)
case apiv1.GenericHMACProvider:
n, err = NewForwarder(f.URL, f.ProxyURL, f.Headers, f.CertPool, []byte(f.Token))
case apiv1.SlackProvider:
n, err = NewSlack(f.URL, f.ProxyURL, f.Token, f.CertPool, f.Username, f.Channel)
case apiv1.DiscordProvider:
n, err = NewDiscord(f.URL, f.ProxyURL, f.Username, f.Channel)
case apiv1.RocketProvider:
n, err = NewRocket(f.URL, f.ProxyURL, f.CertPool, f.Username, f.Channel)
case apiv1.MSTeamsProvider:
n, err = NewMSTeams(f.URL, f.ProxyURL, f.CertPool)
case apiv1.GitHubProvider:
n, err = NewGitHub(f.ProviderUID, f.URL, f.Token, f.CertPool)
case apiv1.GitHubDispatchProvider:
n, err = NewGitHubDispatch(f.URL, f.Token, f.CertPool)
case apiv1.GitLabProvider:
n, err = NewGitLab(f.ProviderUID, f.URL, f.Token, f.CertPool)
case apiv1.GiteaProvider:
n, err = NewGitea(f.ProviderUID, f.URL, f.Token, f.CertPool)
case apiv1.BitbucketServerProvider:
n, err = NewBitbucketServer(f.ProviderUID, f.URL, f.Token, f.CertPool, f.Username, f.Password)
case apiv1.BitbucketProvider:
n, err = NewBitbucket(f.ProviderUID, f.URL, f.Token, f.CertPool)
case apiv1.AzureDevOpsProvider:
n, err = NewAzureDevOps(f.ProviderUID, f.URL, f.Token, f.CertPool)
case apiv1.GoogleChatProvider:
n, err = NewGoogleChat(f.URL, f.ProxyURL)
case apiv1.GooglePubSubProvider:
n, err = NewGooglePubSub(f.URL, f.Channel, f.Token, f.Headers)
case apiv1.WebexProvider:
n, err = NewWebex(f.URL, f.ProxyURL, f.CertPool, f.Channel, f.Token)
case apiv1.SentryProvider:
n, err = NewSentry(f.CertPool, f.URL, f.Channel)
case apiv1.AzureEventHubProvider:
n, err = NewAzureEventHub(f.URL, f.Token, f.Channel)
case apiv1.TelegramProvider:
n, err = NewTelegram(f.Channel, f.Token)
case apiv1.LarkProvider:
n, err = NewLark(f.URL)
case apiv1.Matrix:
n, err = NewMatrix(f.URL, f.Token, f.Channel, f.CertPool)
case apiv1.OpsgenieProvider:
n, err = NewOpsgenie(f.URL, f.ProxyURL, f.CertPool, f.Token)
case apiv1.AlertManagerProvider:
n, err = NewAlertmanager(f.URL, f.ProxyURL, f.CertPool)
case apiv1.GrafanaProvider:
n, err = NewGrafana(f.URL, f.ProxyURL, f.Token, f.CertPool, f.Username, f.Password)
case apiv1.PagerDutyProvider:
n, err = NewPagerDuty(f.URL, f.ProxyURL, f.CertPool, f.Channel)
case apiv1.DataDogProvider:
n, err = NewDataDog(f.URL, f.ProxyURL, f.CertPool, f.Token)
case apiv1.NATSProvider:
n, err = NewNATS(f.URL, f.Channel, f.Username, f.Password)
default:
err = fmt.Errorf("provider %s not supported", provider)
}
return NewGitHub(opts.CommitStatus, opts.URL, opts.Token, opts.TLSConfig, opts.ProxyURL, opts.ProviderName, opts.ProviderNamespace, opts.SecretData, opts.TokenCache)
}
func gitHubDispatchNotifierFunc(opts notifierOptions) (Interface, error) {
if opts.Token == "" && opts.Password != "" {
opts.Token = opts.Password
if err != nil {
n = &NopNotifier{}
}
return NewGitHubDispatch(opts.URL, opts.Token, opts.TLSConfig, opts.ProxyURL, opts.ProviderName, opts.ProviderNamespace, opts.SecretData, opts.TokenCache)
}
func gitLabNotifierFunc(opts notifierOptions) (Interface, error) {
if opts.Token == "" && opts.Password != "" {
opts.Token = opts.Password
}
return NewGitLab(opts.CommitStatus, opts.URL, opts.Token, opts.TLSConfig)
}
func giteaNotifierFunc(opts notifierOptions) (Interface, error) {
if opts.Token == "" && opts.Password != "" {
opts.Token = opts.Password
}
return NewGitea(opts.CommitStatus, opts.URL, opts.ProxyURL, opts.Token, opts.TLSConfig)
}
func bitbucketServerNotifierFunc(opts notifierOptions) (Interface, error) {
return NewBitbucketServer(opts.CommitStatus, opts.URL, opts.Token, opts.TLSConfig, opts.Username, opts.Password)
}
func bitbucketNotifierFunc(opts notifierOptions) (Interface, error) {
return NewBitbucket(opts.CommitStatus, opts.URL, opts.Token, opts.TLSConfig)
}
func azureDevOpsNotifierFunc(opts notifierOptions) (Interface, error) {
return NewAzureDevOps(opts.Context, opts.CommitStatus, opts.URL, opts.Token,
opts.TLSConfig, opts.ProxyURL, opts.ServiceAccountName, opts.ProviderName,
opts.ProviderNamespace, opts.TokenClient, opts.TokenCache)
return n, err
}

View File

@ -20,7 +20,7 @@ import (
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/url"
@ -37,14 +37,14 @@ const NotificationHeader = "gotk-component"
// Forwarder is an implementation of the notification Interface that posts the
// body as an HTTP request using an optional proxy.
type Forwarder struct {
URL string
ProxyURL string
Headers map[string]string
TLSConfig *tls.Config
HMACKey []byte
URL string
ProxyURL string
Headers map[string]string
CertPool *x509.CertPool
HMACKey []byte
}
func NewForwarder(hookURL string, proxyURL string, headers map[string]string, tlsConfig *tls.Config, hmacKey []byte) (*Forwarder, error) {
func NewForwarder(hookURL string, proxyURL string, headers map[string]string, certPool *x509.CertPool, hmacKey []byte) (*Forwarder, error) {
if _, err := url.ParseRequestURI(hookURL); err != nil {
return nil, fmt.Errorf("invalid hook URL %s: %w", hookURL, err)
}
@ -54,11 +54,11 @@ func NewForwarder(hookURL string, proxyURL string, headers map[string]string, tl
}
return &Forwarder{
URL: hookURL,
ProxyURL: proxyURL,
Headers: headers,
HMACKey: hmacKey,
TLSConfig: tlsConfig,
URL: hookURL,
ProxyURL: proxyURL,
Headers: headers,
CertPool: certPool,
HMACKey: hmacKey,
}, nil
}
@ -77,28 +77,18 @@ func (f *Forwarder) Post(ctx context.Context, event eventv1.Event) error {
}
sig = fmt.Sprintf("sha256=%s", sign(eventJSON, f.HMACKey))
}
err := postMessage(ctx, f.URL, f.ProxyURL, f.CertPool, event, func(req *retryablehttp.Request) {
req.Header.Set(NotificationHeader, event.ReportingController)
for key, val := range f.Headers {
req.Header.Set(key, val)
}
if sig != "" {
req.Header.Set("X-Signature", sig)
}
})
opts := []postOption{
withRequestModifier(func(req *retryablehttp.Request) {
req.Header.Set(NotificationHeader, event.ReportingController)
for key, val := range f.Headers {
req.Header.Set(key, val)
}
if sig != "" {
req.Header.Set("X-Signature", sig)
}
}),
}
if f.ProxyURL != "" {
opts = append(opts, withProxy(f.ProxyURL))
}
if f.TLSConfig != nil {
opts = append(opts, withTLSConfig(f.TLSConfig))
}
if err := postMessage(ctx, f.URL, event, opts...); err != nil {
if err != nil {
return fmt.Errorf("postMessage failed: %w", err)
}
return nil
}

View File

@ -18,7 +18,7 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
@ -41,13 +41,13 @@ func Fuzz_Forwarder(f *testing.F) {
}))
defer ts.Close()
var tlsConfig tls.Config
_ = fuzz.NewConsumer(seed).GenerateStruct(&tlsConfig)
var cert x509.CertPool
_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
header := make(map[string]string)
_ = fuzz.NewConsumer(seed).FuzzMap(&header)
forwarder, err := NewForwarder(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", header, &tlsConfig, hmacKey)
forwarder, err := NewForwarder(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", header, &cert, hmacKey)
if err != nil {
return
}

View File

@ -19,6 +19,7 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net/http"
@ -33,27 +34,22 @@ import (
)
type Gitea struct {
BaseURL string
Token string
Owner string
Repo string
CommitStatus string
Client *gitea.Client
Debug bool
BaseURL string
Token string
Owner string
Repo string
ProviderUID string
Client *gitea.Client
Debug bool
}
var _ Interface = &Gitea{}
func NewGitea(commitStatus string, addr string, proxyURL string, token string, tlsConfig *tls.Config) (*Gitea, error) {
func NewGitea(providerUID string, addr string, token string, certPool *x509.CertPool) (*Gitea, error) {
if len(token) == 0 {
return nil, errors.New("gitea token cannot be empty")
}
// this should never happen
if commitStatus == "" {
return nil, errors.New("commit status cannot be empty")
}
host, id, err := parseGitAddress(addr)
if err != nil {
return nil, fmt.Errorf("failed parsing Git URL: %w", err)
@ -68,37 +64,33 @@ func NewGitea(commitStatus string, addr string, proxyURL string, token string, t
return nil, fmt.Errorf("invalid repository id %q", id)
}
tr := &http.Transport{}
if tlsConfig != nil {
tr.TLSClientConfig = tlsConfig
}
if proxyURL != "" {
parsedProxyURL, err := url.Parse(proxyURL)
if err != nil {
return nil, errors.New("invalid proxy URL")
}
tr.Proxy = http.ProxyURL(parsedProxyURL)
}
client, err := gitea.NewClient(host, gitea.SetToken(token), gitea.SetHTTPClient(&http.Client{Transport: tr}))
client, err := gitea.NewClient(host, gitea.SetToken(token))
if err != nil {
return nil, fmt.Errorf("failed creating Gitea client: %w", err)
}
if certPool != nil {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
}
client.SetHTTPClient(&http.Client{Transport: tr})
}
return &Gitea{
BaseURL: host,
Token: token,
Owner: idComponents[0],
Repo: idComponents[1],
CommitStatus: commitStatus,
Client: client,
Debug: os.Getenv("NOTIFIER_GITEA_DEBUG") == "true",
BaseURL: host,
Token: token,
Owner: idComponents[0],
Repo: idComponents[1],
ProviderUID: providerUID,
Client: client,
Debug: os.Getenv("NOTIFIER_GITEA_DEBUG") == "true",
}, nil
}
func (g *Gitea) Post(ctx context.Context, event eventv1.Event) error {
revString, ok := event.GetRevision()
revString, ok := event.Metadata[eventv1.MetaRevisionKey]
if !ok {
return errors.New("missing revision metadata")
}
@ -112,7 +104,7 @@ func (g *Gitea) Post(ctx context.Context, event eventv1.Event) error {
}
_, desc := formatNameAndDescription(event)
id := g.CommitStatus
id := generateCommitStatusID(g.ProviderUID, event)
status := gitea.CreateStatusOption{
State: state,

View File

@ -18,10 +18,7 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
testproxy "github.com/fluxcd/notification-controller/tests/proxy"
"net/http"
"net/http/httptest"
"testing"
@ -34,20 +31,10 @@ import (
"github.com/stretchr/testify/assert"
)
// newTestHTTPServer returns an HTTP server mimicking parts of Gitea's API so that tests don't
// newTestServer returns an HTTP server mimicking parts of Gitea's API so that tests don't
// need to rely on 3rd-party components to be available (like the try.gitea.io server).
func newTestHTTPServer(t *testing.T) *httptest.Server {
return httptest.NewServer(newGiteaStubHandler(t))
}
// newTestHTTPSServer returns an HTTPS server mimicking parts of Gitea's API so that tests don't
// need to rely on 3rd-party components to be available (like the try.gitea.io server).
func newTestHTTPSServer(t *testing.T) *httptest.Server {
return httptest.NewTLSServer(newGiteaStubHandler(t))
}
func newGiteaStubHandler(t *testing.T) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
func newTestServer(t *testing.T) *httptest.Server {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/version":
fmt.Fprintf(w, `{"version":"1.18.3"}`)
@ -55,79 +42,18 @@ func newGiteaStubHandler(t *testing.T) http.Handler {
fmt.Fprintf(w, "[]")
case "/api/v1/repos/foo/bar/statuses/69b59063470310ebbd88a9156325322a124e55a3":
fmt.Fprintf(w, "{}")
case "/api/v1/repos/foo/bar/commits/8a9156325322a124e55a369b59063470310ebbd8/statuses":
fmt.Fprintf(w, "[]")
case "/api/v1/repos/foo/bar/statuses/8a9156325322a124e55a369b59063470310ebbd8":
fmt.Fprintf(w, "{}")
default:
t.Logf("unknown %s request at %s", r.Method, r.URL.Path)
}
})
}))
return srv
}
func TestNewGiteaBasic(t *testing.T) {
srv := newTestHTTPServer(t)
srv := newTestServer(t)
defer srv.Close()
g, err := NewGitea("kustomization/gitops-system/0c9c2e41", srv.URL+"/foo/bar", "", "foobar", nil)
assert.NoError(t, err)
assert.Equal(t, g.Owner, "foo")
assert.Equal(t, g.Repo, "bar")
assert.Equal(t, g.BaseURL, srv.URL)
}
func TestNewGiteaWithCertPool(t *testing.T) {
srv := newTestHTTPSServer(t)
defer srv.Close()
certPool := x509.NewCertPool()
certPool.AddCert(srv.Certificate())
tlsConfig := &tls.Config{RootCAs: certPool}
g, err := NewGitea("kustomization/gitops-system/0c9c2e41", srv.URL+"/foo/bar", "", "foobar", tlsConfig)
assert.NoError(t, err)
assert.Equal(t, g.Owner, "foo")
assert.Equal(t, g.Repo, "bar")
assert.Equal(t, g.BaseURL, srv.URL)
}
func TestNewGiteaNoCertificate(t *testing.T) {
srv := newTestHTTPSServer(t)
defer srv.Close()
certPool := x509.NewCertPool()
tlsConfig := &tls.Config{RootCAs: certPool}
_, err := NewGitea("kustomization/gitops-system/0c9c2e41", srv.URL+"/foo/bar", "", "foobar", tlsConfig)
assert.Error(t, err)
assert.ErrorContains(t, err, "tls: failed to verify certificate: x509: certificate signed by unknown authority")
}
func TestNewGiteaWithProxyURL(t *testing.T) {
srv := newTestHTTPServer(t)
defer srv.Close()
proxyAddr, _ := testproxy.New(t)
proxyURL := fmt.Sprintf("http://%s", proxyAddr)
g, err := NewGitea("kustomization/gitops-system/0c9c2e41", srv.URL+"/foo/bar", proxyURL, "foobar", nil)
assert.NoError(t, err)
assert.Equal(t, g.Owner, "foo")
assert.Equal(t, g.Repo, "bar")
assert.Equal(t, g.BaseURL, srv.URL)
}
func TestNewGiteaWithProxyURLAndCertPool(t *testing.T) {
srv := newTestHTTPSServer(t)
defer srv.Close()
certPool := x509.NewCertPool()
certPool.AddCert(srv.Certificate())
tlsConfig := &tls.Config{RootCAs: certPool}
proxyAddr, _ := testproxy.New(t)
proxyURL := fmt.Sprintf("http://%s", proxyAddr)
g, err := NewGitea("kustomization/gitops-system/0c9c2e41", srv.URL+"/foo/bar", proxyURL, "foobar", tlsConfig)
g, err := NewGitea("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", srv.URL+"/foo/bar", "foobar", nil)
assert.NoError(t, err)
assert.Equal(t, g.Owner, "foo")
assert.Equal(t, g.Repo, "bar")
@ -135,88 +61,44 @@ func TestNewGiteaWithProxyURLAndCertPool(t *testing.T) {
}
func TestNewGiteaInvalidUrl(t *testing.T) {
srv := newTestHTTPServer(t)
srv := newTestServer(t)
defer srv.Close()
_, err := NewGitea("kustomization/gitops-system/0c9c2e41", srv.URL+"/foo/bar/baz", "", "foobar", nil)
_, err := NewGitea("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", srv.URL+"/foo/bar/baz", "foobar", nil)
assert.ErrorContains(t, err, "invalid repository id")
}
func TestNewGiteaInvalidProxyUrl(t *testing.T) {
_, err := NewGitea("kustomization/gitops-system/0c9c2e41", "/foo/bar", "wrong\nURL", "foobar", nil)
assert.ErrorContains(t, err, "invalid proxy URL")
}
func TestNewGiteaEmptyToken(t *testing.T) {
srv := newTestHTTPServer(t)
srv := newTestServer(t)
defer srv.Close()
_, err := NewGitea("kustomization/gitops-system/0c9c2e41", srv.URL+"/foo/bar", "", "", nil)
_, err := NewGitea("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", srv.URL+"/foo/bar", "", nil)
assert.ErrorContains(t, err, "gitea token cannot be empty")
}
func TestNewGiteaEmptyCommitStatus(t *testing.T) {
srv := newTestHTTPServer(t)
defer srv.Close()
_, err := NewGitea("", srv.URL+"/foo/bar", "", "foobar", nil)
assert.ErrorContains(t, err, "commit status cannot be empty")
}
func TestGitea_Post(t *testing.T) {
srv := newTestHTTPServer(t)
srv := newTestServer(t)
defer srv.Close()
g, err := NewGitea("kustomization/gitops-system/0c9c2e41", srv.URL+"/foo/bar", "", "foobar", nil)
g, err := NewGitea("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", srv.URL+"/foo/bar", "foobar", nil)
assert.Nil(t, err)
for _, tt := range []struct {
name string
event eventv1.Event
}{
{
name: "revision key",
event: eventv1.Event{
InvolvedObject: corev1.ObjectReference{
Kind: "Kustomization",
Namespace: "flux-system",
Name: "podinfo-repo",
},
Severity: "info",
Timestamp: metav1.Time{
Time: time.Now(),
},
Metadata: map[string]string{
eventv1.MetaRevisionKey: "main@sha1:69b59063470310ebbd88a9156325322a124e55a3",
},
Message: "Service/podinfo/podinfo configured",
Reason: "",
},
event := eventv1.Event{
InvolvedObject: corev1.ObjectReference{
Kind: "Kustomization",
Namespace: "flux-system",
Name: "podinfo-repo",
},
{
name: "origin revision key",
event: eventv1.Event{
InvolvedObject: corev1.ObjectReference{
Kind: "Kustomization",
Namespace: "flux-system",
Name: "podinfo-repo",
},
Severity: "info",
Timestamp: metav1.Time{
Time: time.Now(),
},
Metadata: map[string]string{
eventv1.MetaRevisionKey: "main@sha1:69b59063470310ebbd88a9156325322a124e55a3",
eventv1.MetaOriginRevisionKey: "main@sha1:8a9156325322a124e55a369b59063470310ebbd8",
},
Message: "Service/podinfo/podinfo configured",
Reason: "",
},
Severity: "info",
Timestamp: metav1.Time{
Time: time.Now(),
},
} {
t.Run(tt.name, func(t *testing.T) {
err := g.Post(context.Background(), tt.event)
assert.NoError(t, err)
})
Metadata: map[string]string{
eventv1.MetaRevisionKey: "main@sha1:69b59063470310ebbd88a9156325322a124e55a3",
},
Message: "Service/podinfo/podinfo configured",
Reason: "",
}
err = g.Post(context.Background(), event)
assert.NoError(t, err)
}

View File

@ -19,43 +19,73 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/google/go-github/v64/github"
"github.com/google/go-github/v53/github"
"golang.org/x/oauth2"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/cache"
)
type GitHub struct {
Owner string
Repo string
CommitStatus string
Client *github.Client
Owner string
Repo string
ProviderUID string
Client *github.Client
}
func NewGitHub(commitStatus string, addr string, token string, tlsConfig *tls.Config,
proxyURL string, providerName string, providerNamespace string, secretData map[string][]byte,
tokenCache *cache.TokenCache) (*GitHub, error) {
// this should never happen
if commitStatus == "" {
return nil, errors.New("commit status cannot be empty")
func NewGitHub(providerUID string, addr string, token string, certPool *x509.CertPool) (*GitHub, error) {
if len(token) == 0 {
return nil, errors.New("github token cannot be empty")
}
repoInfo, err := getRepoInfoAndGithubClient(addr, token, tlsConfig,
proxyURL, providerName, providerNamespace, secretData, tokenCache)
host, id, err := parseGitAddress(addr)
if err != nil {
return nil, err
}
baseUrl, err := url.Parse(host)
if err != nil {
return nil, err
}
comp := strings.Split(id, "/")
if len(comp) != 2 {
return nil, fmt.Errorf("invalid repository id %q", id)
}
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(context.Background(), ts)
client := github.NewClient(tc)
if baseUrl.Host != "github.com" {
if certPool != nil {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
}
hc := &http.Client{Transport: tr}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, hc)
tc = oauth2.NewClient(ctx, ts)
}
client, err = github.NewEnterpriseClient(host, host, tc)
if err != nil {
return nil, fmt.Errorf("could not create enterprise GitHub client: %v", err)
}
}
return &GitHub{
Owner: repoInfo.owner,
Repo: repoInfo.repo,
CommitStatus: commitStatus,
Client: repoInfo.client,
Owner: comp[0],
Repo: comp[1],
ProviderUID: providerUID,
Client: client,
}, nil
}
@ -66,7 +96,7 @@ func (g *GitHub) Post(ctx context.Context, event eventv1.Event) error {
return nil
}
revString, ok := event.GetRevision()
revString, ok := event.Metadata[eventv1.MetaRevisionKey]
if !ok {
return errors.New("missing revision metadata")
}
@ -80,7 +110,7 @@ func (g *GitHub) Post(ctx context.Context, event eventv1.Event) error {
}
_, desc := formatNameAndDescription(event)
id := g.CommitStatus
id := generateCommitStatusID(g.ProviderUID, event)
status := &github.RepoStatus{
State: &state,
Context: &id,

View File

@ -19,13 +19,18 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/cache"
"github.com/google/go-github/v64/github"
"github.com/google/go-github/v53/github"
"golang.org/x/oauth2"
)
type GitHubDispatch struct {
@ -34,20 +39,51 @@ type GitHubDispatch struct {
Client *github.Client
}
func NewGitHubDispatch(addr string, token string, tlsConfig *tls.Config, proxyURL string,
providerName string, providerNamespace string, secretData map[string][]byte,
tokenCache *cache.TokenCache) (*GitHubDispatch, error) {
func NewGitHubDispatch(addr string, token string, certPool *x509.CertPool) (*GitHubDispatch, error) {
if len(token) == 0 {
return nil, errors.New("github token cannot be empty")
}
repoInfo, err := getRepoInfoAndGithubClient(addr, token, tlsConfig,
proxyURL, providerName, providerNamespace, secretData, tokenCache)
host, id, err := parseGitAddress(addr)
if err != nil {
return nil, err
}
baseUrl, err := url.Parse(host)
if err != nil {
return nil, err
}
comp := strings.Split(id, "/")
if len(comp) != 2 {
return nil, fmt.Errorf("invalid repository id %q", id)
}
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(context.Background(), ts)
client := github.NewClient(tc)
if baseUrl.Host != "github.com" {
if certPool != nil {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
}
hc := &http.Client{Transport: tr}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, hc)
tc = oauth2.NewClient(ctx, ts)
}
client, err = github.NewEnterpriseClient(host, host, tc)
if err != nil {
return nil, fmt.Errorf("could not create enterprise GitHub client: %v", err)
}
}
return &GitHubDispatch{
Owner: repoInfo.owner,
Repo: repoInfo.repo,
Client: repoInfo.client,
Owner: comp[0],
Repo: comp[1],
Client: client,
}, nil
}

View File

@ -18,7 +18,6 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
@ -47,8 +46,7 @@ func Fuzz_GitHub_Dispatch(f *testing.F) {
var cert x509.CertPool
_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
tlsConfig := &tls.Config{RootCAs: &cert}
dispatch, err := NewGitHubDispatch(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, tlsConfig, "", "", "", nil, nil)
dispatch, err := NewGitHubDispatch(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert)
if err != nil {
return
}

View File

@ -18,23 +18,16 @@ package notifier
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
authgithub "github.com/fluxcd/pkg/git/github"
"github.com/fluxcd/pkg/ssh"
)
func TestNewGitHubDispatchBasic(t *testing.T) {
g, err := NewGitHubDispatch("https://github.com/foo/bar", "foobar", nil, "", "", "", nil, nil)
g, err := NewGitHubDispatch("https://github.com/foo/bar", "foobar", nil)
assert.Nil(t, err)
assert.Equal(t, g.Owner, "foo")
assert.Equal(t, g.Repo, "bar")
@ -42,7 +35,7 @@ func TestNewGitHubDispatchBasic(t *testing.T) {
}
func TestNewEnterpriseGitHubDispatchBasic(t *testing.T) {
g, err := NewGitHubDispatch("https://foobar.com/foo/bar", "foobar", nil, "", "", "", nil, nil)
g, err := NewGitHubDispatch("https://foobar.com/foo/bar", "foobar", nil)
assert.Nil(t, err)
assert.Equal(t, g.Owner, "foo")
assert.Equal(t, g.Repo, "bar")
@ -50,98 +43,17 @@ func TestNewEnterpriseGitHubDispatchBasic(t *testing.T) {
}
func TestNewGitHubDispatchInvalidUrl(t *testing.T) {
_, err := NewGitHubDispatch("https://github.com/foo/bar/baz", "foobar", nil, "", "", "", nil, nil)
_, err := NewGitHubDispatch("https://github.com/foo/bar/baz", "foobar", nil)
assert.NotNil(t, err)
}
func TestNewGitHubDispatchEmptyToken(t *testing.T) {
_, err := NewGitHubDispatch("https://github.com/foo/bar", "", nil, "", "", "", nil, nil)
_, err := NewGitHubDispatch("https://github.com/foo/bar", "", nil)
assert.NotNil(t, err)
}
func TestNewGithubDispatchProvider(t *testing.T) {
appID := "123"
installationID := "456"
kp, _ := ssh.GenerateKeyPair(ssh.RSA_4096)
expiresAt := time.Now().UTC().Add(time.Hour)
for _, tt := range []struct {
name string
secretData map[string][]byte
wantErr error
}{
{
name: "nil provider, no token",
wantErr: errors.New("github token or github app details must be specified"),
},
{
name: "provider with no github options",
secretData: map[string][]byte{},
wantErr: errors.New("github token or github app details must be specified"),
},
{
name: "provider with missing app ID in options ",
secretData: map[string][]byte{
"githubAppInstallationID": []byte(installationID),
"githubAppPrivateKey": kp.PrivateKey,
},
wantErr: errors.New("github token or github app details must be specified"),
},
{
name: "provider with missing app installation ID in options ",
secretData: map[string][]byte{
"githubAppID": []byte(appID),
"githubAppPrivateKey": kp.PrivateKey,
},
wantErr: errors.New("app installation ID must be provided to use github app authentication"),
},
{
name: "provider with missing app private key in options ",
secretData: map[string][]byte{
"githubAppID": []byte(appID),
"githubAppInstallationID": []byte(installationID),
},
wantErr: errors.New("private key must be provided to use github app authentication"),
},
{
name: "provider with complete app authentication information",
secretData: map[string][]byte{
"githubAppID": []byte(appID),
"githubAppInstallationID": []byte(installationID),
"githubAppPrivateKey": kp.PrivateKey,
},
},
} {
t.Run(tt.name, func(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
var response []byte
var err error
response, err = json.Marshal(&authgithub.AppToken{Token: "access-token", ExpiresAt: expiresAt})
assert.Nil(t, err)
w.Write(response)
}
srv := httptest.NewServer(http.HandlerFunc(handler))
t.Cleanup(func() {
srv.Close()
})
if len(tt.secretData) > 0 {
tt.secretData["githubAppBaseURL"] = []byte(srv.URL)
}
_, err := NewGitHubDispatch("https://github.com/foo/bar", "", nil, "", "foo", "bar", tt.secretData, nil)
if tt.wantErr != nil {
assert.NotNil(t, err)
assert.Equal(t, tt.wantErr, err)
} else {
assert.Nil(t, err)
}
})
}
}
func TestGitHubDispatch_PostUpdate(t *testing.T) {
githubDispatch, err := NewGitHubDispatch("https://github.com/foo/bar", "foobar", nil, "", "", "", nil, nil)
githubDispatch, err := NewGitHubDispatch("https://github.com/foo/bar", "foobar", nil)
require.NoError(t, err)
event := testEvent()

View File

@ -18,7 +18,6 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
@ -31,14 +30,14 @@ import (
)
func Fuzz_GitHub(f *testing.F) {
f.Add("kustomization/gitops-system/0c9c2e41", "token", "org/repo", "revision/abce1", "error", "", []byte{}, []byte(`[{"context":"/","state":"failure","description":""}]`))
f.Add("kustomization/gitops-system/0c9c2e41", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/","state":"success","description":""}]`))
f.Add("kustomization/gitops-system/0c9c2e41", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/","state":"failure","description":""}]`))
f.Add("kustomization/gitops-system/0c9c2e41", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/"}]`))
f.Add("kustomization/gitops-system/0c9c2e41", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{}]`))
f.Add("kustomization/gitops-system/0c9c2e41", "token", "org/repo", "revision/abce1", "info", "Progressing", []byte{}, []byte{})
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "error", "", []byte{}, []byte(`[{"context":"/","state":"failure","description":""}]`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/","state":"success","description":""}]`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/","state":"failure","description":""}]`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"context":"/"}]`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{}]`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "Progressing", []byte{}, []byte{})
f.Fuzz(func(t *testing.T, commitStatus, token, urlSuffix, revision, severity, reason string, seed, response []byte) {
f.Fuzz(func(t *testing.T, uuid, token, urlSuffix, revision, severity, reason string, seed, response []byte) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(response)
io.Copy(io.Discard, r.Body)
@ -49,8 +48,7 @@ func Fuzz_GitHub(f *testing.F) {
var cert x509.CertPool
_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
tlsConfig := &tls.Config{RootCAs: &cert}
github, err := NewGitHub(commitStatus, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, tlsConfig, "", "foo", "bar", nil, nil)
github, err := NewGitHub(uuid, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert)
if err != nil {
return
}

View File

@ -1,134 +0,0 @@
/*
Copyright 2025 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 notifier
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
gogithub "github.com/google/go-github/v64/github"
"golang.org/x/oauth2"
"github.com/fluxcd/pkg/cache"
"github.com/fluxcd/pkg/git/github"
"github.com/fluxcd/notification-controller/api/v1beta3"
)
// repoInfo is an internal type encapsulating owner, repo and client
type repoInfo struct {
owner string
repo string
client *gogithub.Client
}
// getGitHubAppOptions constructs the github app authentication options.
func getGitHubAppOptions(providerName, providerNamespace, proxy string,
secretData map[string][]byte, tokenCache *cache.TokenCache) ([]github.OptFunc, error) {
githubOpts := []github.OptFunc{
github.WithAppData(secretData),
}
if len(githubOpts) > 0 && proxy != "" {
proxyURL, err := url.Parse(proxy)
if err != nil {
return nil, fmt.Errorf("error parsing proxy URL '%s': %w", proxy, err)
}
githubOpts = append(githubOpts, github.WithProxyURL(proxyURL))
}
if len(githubOpts) > 0 && tokenCache != nil {
githubOpts = append(githubOpts, github.WithCache(tokenCache,
v1beta3.ProviderKind, providerName, providerNamespace, OperationPost))
}
return githubOpts, nil
}
// getRepoInfoAndGithubClient gets the github client and repository info used by Github and GithubDispatch providers
func getRepoInfoAndGithubClient(addr string, token string, tlsConfig *tls.Config,
proxyURL string, providerName string, providerNamespace string,
secretData map[string][]byte, tokenCache *cache.TokenCache) (*repoInfo, error) {
if len(token) == 0 {
if _, ok := secretData[github.KeyAppID]; !ok {
return nil, errors.New("github token or github app details must be specified")
}
githubOpts, err := getGitHubAppOptions(providerName, providerNamespace, proxyURL, secretData, tokenCache)
if err != nil {
return nil, err
}
if tlsConfig != nil {
// add TLS config to get installation token
githubOpts = append(githubOpts, github.WithTLSConfig(tlsConfig))
}
client, err := github.New(githubOpts...)
if err != nil {
return nil, err
}
appToken, err := client.GetToken(context.Background())
if err != nil {
return nil, err
}
token = appToken.Token
}
host, id, err := parseGitAddress(addr)
if err != nil {
return nil, err
}
comp := strings.Split(id, "/")
if len(comp) != 2 {
return nil, fmt.Errorf("invalid repository id %q", id)
}
baseUrl, err := url.Parse(host)
if err != nil {
return nil, err
}
ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
tc := oauth2.NewClient(context.Background(), ts)
client := gogithub.NewClient(tc)
if baseUrl.Host != "github.com" {
if tlsConfig != nil {
tr := &http.Transport{
TLSClientConfig: tlsConfig,
}
hc := &http.Client{Transport: tr}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, hc)
tc = oauth2.NewClient(ctx, ts)
}
client, err = gogithub.NewClient(tc).WithEnterpriseURLs(host, host)
if err != nil {
return nil, fmt.Errorf("could not create enterprise GitHub client: %v", err)
}
}
return &repoInfo{comp[0], comp[1], client}, nil
}

View File

@ -17,134 +17,38 @@ limitations under the License.
package notifier
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
authgithub "github.com/fluxcd/pkg/git/github"
"github.com/fluxcd/pkg/ssh"
"github.com/google/go-github/v64/github"
"github.com/google/go-github/v53/github"
"github.com/stretchr/testify/assert"
)
func TestNewGitHubBasic(t *testing.T) {
g, err := NewGitHub("kustomization/gitops-system/0c9c2e41", "https://github.com/foo/bar", "foobar", nil, "", "", "", nil, nil)
g, err := NewGitHub("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://github.com/foo/bar", "foobar", nil)
assert.Nil(t, err)
assert.Equal(t, g.Owner, "foo")
assert.Equal(t, g.Repo, "bar")
assert.Equal(t, g.Client.BaseURL.Host, "api.github.com")
assert.Equal(t, g.CommitStatus, "kustomization/gitops-system/0c9c2e41")
}
func TestNewEmterpriseGitHubBasic(t *testing.T) {
g, err := NewGitHub("kustomization/gitops-system/0c9c2e41", "https://foobar.com/foo/bar", "foobar", nil, "", "", "", nil, nil)
g, err := NewGitHub("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://foobar.com/foo/bar", "foobar", nil)
assert.Nil(t, err)
assert.Equal(t, g.Owner, "foo")
assert.Equal(t, g.Repo, "bar")
assert.Equal(t, g.Client.BaseURL.Host, "foobar.com")
assert.Equal(t, g.CommitStatus, "kustomization/gitops-system/0c9c2e41")
}
func TestNewGitHubInvalidUrl(t *testing.T) {
_, err := NewGitHub("kustomization/gitops-system/0c9c2e41", "https://github.com/foo/bar/baz", "foobar", nil, "", "", "", nil, nil)
_, err := NewGitHub("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://github.com/foo/bar/baz", "foobar", nil)
assert.NotNil(t, err)
}
func TestNewGitHubEmptyToken(t *testing.T) {
_, err := NewGitHub("kustomization/gitops-system/0c9c2e41", "https://github.com/foo/bar", "", nil, "", "", "", nil, nil)
_, err := NewGitHub("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://github.com/foo/bar", "", nil)
assert.NotNil(t, err)
}
func TestNewGitHubEmptyCommitStatus(t *testing.T) {
_, err := NewGitHub("", "https://github.com/foo/bar", "foobar", nil, "", "", "", nil, nil)
assert.NotNil(t, err)
}
func TestNewGithubProvider(t *testing.T) {
appID := "123"
installationID := "456"
kp, _ := ssh.GenerateKeyPair(ssh.RSA_4096)
expiresAt := time.Now().UTC().Add(time.Hour)
for _, tt := range []struct {
name string
secretData map[string][]byte
wantErr error
}{
{
name: "nil provider, no token",
wantErr: errors.New("github token or github app details must be specified"),
},
{
name: "provider with no github options",
secretData: map[string][]byte{},
wantErr: errors.New("github token or github app details must be specified"),
},
{
name: "provider with missing app ID in options ",
secretData: map[string][]byte{
"githubAppInstallationID": []byte(installationID),
"githubAppPrivateKey": kp.PrivateKey,
},
wantErr: errors.New("github token or github app details must be specified"),
},
{
name: "provider with missing app installation ID in options ",
secretData: map[string][]byte{
"githubAppID": []byte(appID),
"githubAppPrivateKey": kp.PrivateKey,
},
wantErr: errors.New("app installation ID must be provided to use github app authentication"),
},
{
name: "provider with missing app private key in options ",
secretData: map[string][]byte{
"githubAppID": []byte(appID),
"githubAppInstallationID": []byte(installationID),
},
wantErr: errors.New("private key must be provided to use github app authentication"),
},
{
name: "provider with complete app authentication information",
secretData: map[string][]byte{
"githubAppID": []byte(appID),
"githubAppInstallationID": []byte(installationID),
"githubAppPrivateKey": kp.PrivateKey,
},
},
} {
t.Run(tt.name, func(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
var response []byte
var err error
response, err = json.Marshal(&authgithub.AppToken{Token: "access-token", ExpiresAt: expiresAt})
assert.Nil(t, err)
w.Write(response)
}
srv := httptest.NewServer(http.HandlerFunc(handler))
t.Cleanup(func() {
srv.Close()
})
if len(tt.secretData) > 0 {
tt.secretData["githubAppBaseURL"] = []byte(srv.URL)
}
_, err := NewGitHub("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://github.com/foo/bar", "", nil, "", "foo", "bar", tt.secretData, nil)
if tt.wantErr != nil {
assert.NotNil(t, err)
assert.Equal(t, tt.wantErr, err)
} else {
assert.Nil(t, err)
}
})
}
}
func TestDuplicateGithubStatus(t *testing.T) {
assert := assert.New(t)

View File

@ -19,23 +19,24 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net/http"
gitlab "gitlab.com/gitlab-org/api/client-go"
"github.com/xanzy/go-gitlab"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
"github.com/fluxcd/pkg/apis/meta"
)
type GitLab struct {
Id string
CommitStatus string
Client *gitlab.Client
Id string
ProviderUID string
Client *gitlab.Client
}
func NewGitLab(commitStatus string, addr string, token string, tlsConfig *tls.Config) (*GitLab, error) {
func NewGitLab(providerUID string, addr string, token string, certPool *x509.CertPool) (*GitLab, error) {
if len(token) == 0 {
return nil, errors.New("gitlab token cannot be empty")
}
@ -45,15 +46,12 @@ func NewGitLab(commitStatus string, addr string, token string, tlsConfig *tls.Co
return nil, err
}
// this should never happen
if commitStatus == "" {
return nil, errors.New("commit status cannot be empty")
}
opts := []gitlab.ClientOptionFunc{gitlab.WithBaseURL(host)}
if tlsConfig != nil {
if certPool != nil {
tr := &http.Transport{
TLSClientConfig: tlsConfig,
TLSClientConfig: &tls.Config{
RootCAs: certPool,
},
}
hc := &http.Client{Transport: tr}
opts = append(opts, gitlab.WithHTTPClient(hc))
@ -64,9 +62,9 @@ func NewGitLab(commitStatus string, addr string, token string, tlsConfig *tls.Co
}
gitlab := &GitLab{
Id: id,
CommitStatus: commitStatus,
Client: client,
Id: id,
ProviderUID: providerUID,
Client: client,
}
return gitlab, nil
@ -79,7 +77,7 @@ func (g *GitLab) Post(ctx context.Context, event eventv1.Event) error {
return nil
}
revString, ok := event.GetRevision()
revString, ok := event.Metadata[eventv1.MetaRevisionKey]
if !ok {
return errors.New("missing revision metadata")
}
@ -93,7 +91,7 @@ func (g *GitLab) Post(ctx context.Context, event eventv1.Event) error {
}
_, desc := formatNameAndDescription(event)
id := g.CommitStatus
id := generateCommitStatusID(g.ProviderUID, event)
status := &gitlab.CommitStatus{
Name: id,
SHA: rev,
@ -101,9 +99,7 @@ func (g *GitLab) Post(ctx context.Context, event eventv1.Event) error {
Description: desc,
}
getOpt := &gitlab.GetCommitStatusesOptions{
Name: &status.Name,
}
getOpt := &gitlab.GetCommitStatusesOptions{}
statuses, _, err := g.Client.Commits.GetCommitStatuses(g.Id, rev, getOpt, gitlab.WithContext(ctx))
if err != nil {
return fmt.Errorf("unable to list commit status: %s", err)

View File

@ -18,7 +18,6 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
@ -31,12 +30,12 @@ import (
)
func Fuzz_GitLab(f *testing.F) {
f.Add("kustomization/gitops-system/0c9c2e41", "token", "org/repo", "revision/abce1", "error", "", []byte{}, []byte(`[{"sha":"abce1","status":"failed","name":"/","description":""}]`))
f.Add("kustomization/gitops-system/0c9c2e41", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"sha":"abce1","status":"failed","name":"/","description":""}]`))
f.Add("kustomization/gitops-system/0c9c2e41", "token", "org/repo", "revision/abce1", "info", "Progressing", []byte{}, []byte{})
f.Add("kustomization/gitops-system/0c9c2e41", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[]`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "error", "", []byte{}, []byte(`[{"sha":"abce1","status":"failed","name":"/","description":""}]`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[{"sha":"abce1","status":"failed","name":"/","description":""}]`))
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "Progressing", []byte{}, []byte{})
f.Add("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "token", "org/repo", "revision/abce1", "info", "", []byte{}, []byte(`[]`))
f.Fuzz(func(t *testing.T, commitStatus, token, urlSuffix, revision, severity, reason string, seed, response []byte) {
f.Fuzz(func(t *testing.T, uuid, token, urlSuffix, revision, severity, reason string, seed, response []byte) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(response)
io.Copy(io.Discard, r.Body)
@ -47,8 +46,7 @@ func Fuzz_GitLab(f *testing.F) {
var cert x509.CertPool
_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
tlsConfig := &tls.Config{RootCAs: &cert}
gitLab, err := NewGitLab(commitStatus, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, tlsConfig)
gitLab, err := NewGitLab(uuid, fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, &cert)
if err != nil {
return
}

View File

@ -23,31 +23,25 @@ import (
)
func TestNewGitLabBasic(t *testing.T) {
g, err := NewGitLab("kustomization/gitops-system/0c9c2e41", "https://gitlab.com/foo/bar", "foobar", nil)
g, err := NewGitLab("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://gitlab.com/foo/bar", "foobar", nil)
assert.Nil(t, err)
assert.Equal(t, g.Id, "foo/bar")
assert.Equal(t, g.CommitStatus, "kustomization/gitops-system/0c9c2e41")
}
func TestNewGitLabSubgroups(t *testing.T) {
g, err := NewGitLab("kustomization/gitops-system/0c9c2e41", "https://gitlab.com/foo/bar/baz", "foobar", nil)
g, err := NewGitLab("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://gitlab.com/foo/bar/baz", "foobar", nil)
assert.Nil(t, err)
assert.Equal(t, g.Id, "foo/bar/baz")
}
func TestNewGitLabSelfHosted(t *testing.T) {
g, err := NewGitLab("kustomization/gitops-system/0c9c2e41", "https://example.com/foo/bar", "foo:bar", nil)
g, err := NewGitLab("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://example.com/foo/bar", "foo:bar", nil)
assert.Nil(t, err)
assert.Equal(t, g.Id, "foo/bar")
assert.Equal(t, g.Client.BaseURL().Host, "example.com")
}
func TestNewGitLabEmptyToken(t *testing.T) {
_, err := NewGitLab("kustomization/gitops-system/0c9c2e41", "https://gitlab.com/foo/bar", "", nil)
assert.NotNil(t, err)
}
func TestNewGitLabEmptyCommitStatus(t *testing.T) {
_, err := NewGitLab("", "https://gitlab.com/foo/bar", "foobar", nil)
_, err := NewGitLab("0c9c2e41-d2f9-4f9b-9c41-bebc1984d67a", "https://gitlab.com/foo/bar", "", nil)
assert.NotNil(t, err)
}

View File

@ -142,12 +142,8 @@ func (s *GoogleChat) Post(ctx context.Context, event eventv1.Event) error {
Cards: []GoogleChatCard{card},
}
var opts []postOption
if s.ProxyURL != "" {
opts = append(opts, withProxy(s.ProxyURL))
}
if err := postMessage(ctx, s.URL, payload, opts...); err != nil {
err := postMessage(ctx, s.URL, s.ProxyURL, nil, payload)
if err != nil {
return fmt.Errorf("postMessage failed: %w", err)
}

View File

@ -1,73 +0,0 @@
/*
Copyright 2025 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 notifier
import (
"context"
"fmt"
"net/url"
"google.golang.org/api/option"
"github.com/fluxcd/pkg/auth"
"github.com/fluxcd/pkg/auth/gcp"
"github.com/fluxcd/pkg/cache"
"github.com/fluxcd/notification-controller/api/v1beta3"
)
// buildGCPClientOptions builds client options for GCP services.
// Authentication precedence: JSON credentials take priority over workload identity.
func buildGCPClientOptions(ctx context.Context, opts notifierOptions) ([]option.ClientOption, error) {
var clientOpts []option.ClientOption
if opts.Token != "" {
clientOpts = append(clientOpts, option.WithCredentialsJSON([]byte(opts.Token)))
} else {
authOpts := []auth.Option{
auth.WithClient(opts.TokenClient),
auth.WithServiceAccountNamespace(opts.ProviderNamespace),
}
if opts.TokenCache != nil {
involvedObject := cache.InvolvedObject{
Kind: v1beta3.ProviderKind,
Name: opts.ProviderName,
Namespace: opts.ProviderNamespace,
Operation: OperationPost,
}
authOpts = append(authOpts, auth.WithCache(*opts.TokenCache, involvedObject))
}
if opts.ServiceAccountName != "" {
authOpts = append(authOpts, auth.WithServiceAccountName(opts.ServiceAccountName))
}
if opts.ProxyURL != "" {
proxyURL, err := url.Parse(opts.ProxyURL)
if err != nil {
return nil, fmt.Errorf("error parsing proxy URL: %w", err)
}
authOpts = append(authOpts, auth.WithProxyURL(*proxyURL))
}
tokenSource := gcp.NewTokenSource(ctx, authOpts...)
clientOpts = append(clientOpts, option.WithTokenSource(tokenSource))
}
return clientOpts, nil
}

View File

@ -23,6 +23,7 @@ import (
"fmt"
"cloud.google.com/go/pubsub"
"google.golang.org/api/option"
kerrors "k8s.io/apimachinery/pkg/util/errors"
"sigs.k8s.io/controller-runtime/pkg/log"
@ -32,13 +33,18 @@ import (
type (
// GooglePubSub holds a Google Pub/Sub client and target topic.
GooglePubSub struct {
topicID string
attrs map[string]string
topicName string
client interface {
publish(ctx context.Context, eventPayload []byte) error
publish(ctx context.Context, topicID string, eventPayload []byte, attrs map[string]string) (serverID string, err error)
}
}
googlePubSubClient struct {
opts *notifierOptions
projectID string
jsonCreds []byte
}
)
@ -46,16 +52,33 @@ type (
var _ Interface = &GooglePubSub{}
// NewGooglePubSub creates a Google Pub/Sub client tied to a specific
// project and topic using the provided client options.
func NewGooglePubSub(opts *notifierOptions) (*GooglePubSub, error) {
if opts.URL == "" {
// project and topic.
//
// The jsonCreds parameter is optional, and if len(jsonCreds) == 0 then the
// automatic authentication methods of the Google libraries will take place,
// and therefore methods like Workload Identity will be automatically attempted.
//
// The attrs paramter is optional, and if len(attrs) == 0 then no attributes will
// be added to the Pub/Sub message.
func NewGooglePubSub(projectID, topicID, jsonCreds string, attrs map[string]string) (*GooglePubSub, error) {
if projectID == "" {
return nil, errors.New("GCP project ID cannot be empty")
}
if opts.Channel == "" {
if topicID == "" {
return nil, errors.New("GCP Pub/Sub topic ID cannot be empty")
}
client := &googlePubSubClient{opts}
return &GooglePubSub{client}, nil
if len(attrs) == 0 {
attrs = nil
}
return &GooglePubSub{
topicID: topicID,
attrs: attrs,
topicName: fmt.Sprintf("projects/%s/topics/%s", projectID, topicID),
client: &googlePubSubClient{
projectID: projectID,
jsonCreds: []byte(jsonCreds),
},
}, nil
}
// Post posts Flux events to a Google Pub/Sub topic.
@ -70,21 +93,28 @@ func (g *GooglePubSub) Post(ctx context.Context, event eventv1.Event) error {
return fmt.Errorf("error json-marshaling event: %w", err)
}
return g.client.publish(ctx, eventPayload)
serverID, err := g.client.publish(ctx, g.topicID, eventPayload, g.attrs)
if err != nil {
return fmt.Errorf("error publishing event to topic %s: %w", g.topicName, err)
}
// debug log
log.FromContext(ctx).V(1).Info("Event published to GCP Pub/Sub topic",
"topic", g.topicName,
"server message id", serverID)
return nil
}
func (g *googlePubSubClient) publish(ctx context.Context, eventPayload []byte) error {
projectID := g.opts.URL
topicID := g.opts.Channel
// Build client.
opts, err := buildGCPClientOptions(ctx, *g.opts)
if err != nil {
return err
func (g *googlePubSubClient) publish(ctx context.Context, topicID string, eventPayload []byte, attrs map[string]string) (serverID string, err error) {
var opts []option.ClientOption
if len(g.jsonCreds) > 0 {
opts = append(opts, option.WithCredentialsJSON(g.jsonCreds))
}
client, err := pubsub.NewClient(ctx, projectID, opts...)
var client *pubsub.Client
client, err = pubsub.NewClient(ctx, g.projectID, opts...)
if err != nil {
return err
return
}
defer func() {
if closeErr := client.Close(); closeErr != nil {
@ -95,28 +125,12 @@ func (g *googlePubSubClient) publish(ctx context.Context, eventPayload []byte) e
}
}
}()
// Publish the event to the topic.
attrs := g.opts.Headers
if len(attrs) == 0 {
attrs = nil
}
topic := fmt.Sprintf("projects/%s/topics/%s", projectID, topicID)
serverID, err := client.
serverID, err = client.
Topic(topicID).
Publish(ctx, &pubsub.Message{
Data: eventPayload,
Attributes: attrs,
}).
Get(ctx)
if err != nil {
return fmt.Errorf("error publishing to GCP Pub/Sub topic %s: %w", topic, err)
}
// Emit debug log.
log.FromContext(ctx).V(1).Info("Event published to GCP Pub/Sub topic",
"topic", topic,
"server message id", serverID)
return nil
return
}

View File

@ -19,6 +19,7 @@ package notifier
import (
"context"
"errors"
"fmt"
"testing"
. "github.com/onsi/gomega"
@ -28,10 +29,15 @@ import (
func TestNewGooglePubSub(t *testing.T) {
tests := []struct {
name string
projectID string
topicID string
expectedErr error
name string
projectID string
topicID string
jsonCreds string
attrs map[string]string
expectedErr error
expectedTopicName string
expectedJSONCreds []byte
expectedAttrs map[string]string
}{
{
name: "empty project ID is not allowed",
@ -44,18 +50,61 @@ func TestNewGooglePubSub(t *testing.T) {
topicID: "",
expectedErr: errors.New("GCP Pub/Sub topic ID cannot be empty"),
},
{
name: "topic name is stored properly",
projectID: "project-id",
topicID: "topic-id",
expectedTopicName: "projects/project-id/topics/topic-id",
},
{
name: "json creds are stored properly",
projectID: "project-id",
topicID: "topic-id",
expectedTopicName: "projects/project-id/topics/topic-id",
jsonCreds: "json credentials",
expectedJSONCreds: []byte("json credentials"),
},
{
name: "non-empty attributes are stored properly",
projectID: "project-id",
topicID: "topic-id",
expectedTopicName: "projects/project-id/topics/topic-id",
attrs: map[string]string{"foo": "bar"},
expectedAttrs: map[string]string{"foo": "bar"},
},
{
name: "empty attributes are stored properly",
projectID: "project-id",
topicID: "topic-id",
expectedTopicName: "projects/project-id/topics/topic-id",
attrs: map[string]string{},
expectedAttrs: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
provider, err := NewGooglePubSub(&notifierOptions{
URL: tt.projectID,
Channel: tt.topicID,
})
g.Expect(err).To(Equal(tt.expectedErr))
g.Expect(provider).To(BeNil())
provider, err := NewGooglePubSub(tt.projectID, tt.topicID, tt.jsonCreds, tt.attrs)
if tt.expectedErr != nil {
g.Expect(err).To(Equal(tt.expectedErr))
g.Expect(provider).To(BeNil())
} else {
g.Expect(err).To(BeNil())
g.Expect(provider).NotTo(BeNil())
g.Expect(provider.topicID).To(Equal(tt.topicID))
g.Expect(provider.attrs).To(Equal(tt.expectedAttrs))
g.Expect(provider.topicName).To(Equal(tt.expectedTopicName))
g.Expect(provider.client).NotTo(BeNil())
client := provider.client.(*googlePubSubClient)
g.Expect(client).NotTo(BeNil())
g.Expect(client.projectID).To(Equal(tt.projectID))
g.Expect(client.jsonCreds).To(Equal(tt.expectedJSONCreds))
}
})
}
}
@ -75,11 +124,14 @@ type googlePubSubPostTestCase struct {
g *WithT
}
func (tt *googlePubSubPostTestCase) publish(ctx context.Context, eventPayload []byte) error {
func (tt *googlePubSubPostTestCase) publish(ctx context.Context, topicID string, eventPayload []byte, attrs map[string]string) (serverID string, err error) {
tt.g.THelper()
tt.publishExecuted = true
tt.g.Expect(topicID).To(Equal(tt.topicID))
tt.g.Expect(string(eventPayload)).To(Equal(tt.expectedEventPayload))
return tt.publishErr
tt.g.Expect(attrs).To(Equal(tt.attrs))
// serverID is only used in a debug log for now, there's no way to assert it
return "", tt.publishErr
}
func TestGooglePubSubPost(t *testing.T) {
@ -100,11 +152,11 @@ func TestGooglePubSubPost(t *testing.T) {
publishShouldExecute: false,
},
{
name: "publish error is relayed",
name: "publish error is wrapped and relayed",
expectedEventPayload: `{"involvedObject":{},"severity":"","timestamp":null,"message":"","reason":"","reportingController":""}`,
topicName: "projects/projectID/topics/topicID",
publishErr: errors.New("publish error"),
expectedErr: errors.New("publish error"),
expectedErr: fmt.Errorf("error publishing event to topic projects/projectID/topics/topicID: %w", errors.New("publish error")),
publishShouldExecute: true,
},
{
@ -122,7 +174,10 @@ func TestGooglePubSubPost(t *testing.T) {
tt.g = g
topic := &GooglePubSub{
client: tt,
client: tt,
topicID: tt.topicID,
attrs: tt.attrs,
topicName: tt.topicName,
}
err := topic.Post(context.Background(), tt.event)

View File

@ -18,7 +18,7 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/url"
"strings"
@ -28,15 +28,15 @@ import (
)
type Grafana struct {
URL string
Token string
ProxyURL string
TLSConfig *tls.Config
Username string
Password string
URL string
Token string
ProxyURL string
CertPool *x509.CertPool
Username string
Password string
}
// GraphitePayload represents a Grafana API annotation in Graphite format
// GraphiteAnnotation represents a Grafana API annotation in Graphite format
type GraphitePayload struct {
When int64 `json:"when"` //optional unix timestamp (ms)
Text string `json:"text"`
@ -44,19 +44,19 @@ type GraphitePayload struct {
}
// NewGrafana validates the Grafana URL and returns a Grafana object
func NewGrafana(URL string, proxyURL string, token string, tlsConfig *tls.Config, username string, password string) (*Grafana, error) {
func NewGrafana(URL string, proxyURL string, token string, certPool *x509.CertPool, username string, password string) (*Grafana, error) {
_, err := url.ParseRequestURI(URL)
if err != nil {
return nil, fmt.Errorf("invalid Grafana URL %s", URL)
}
return &Grafana{
URL: URL,
ProxyURL: proxyURL,
Token: token,
Username: username,
Password: password,
TLSConfig: tlsConfig,
URL: URL,
ProxyURL: proxyURL,
Token: token,
CertPool: certPool,
Username: username,
Password: password,
}, nil
}
@ -71,37 +71,23 @@ func (g *Grafana) Post(ctx context.Context, event eventv1.Event) error {
// add tag to filter on grafana
sfields = append(sfields, "flux", event.ReportingController)
for k, v := range event.Metadata {
key := strings.ReplaceAll(k, ":", "|")
value := strings.ReplaceAll(v, ":", "|")
sfields = append(sfields, fmt.Sprintf("%s: %s", key, value))
sfields = append(sfields, fmt.Sprintf("%s: %s", k, v))
}
sfields = append(sfields, fmt.Sprintf("kind: %s", event.InvolvedObject.Kind))
sfields = append(sfields, fmt.Sprintf("name: %s", event.InvolvedObject.Name))
sfields = append(sfields, fmt.Sprintf("namespace: %s", event.InvolvedObject.Namespace))
payload := GraphitePayload{
When: event.Timestamp.Unix(),
Text: fmt.Sprintf("%s/%s.%s", strings.ToLower(event.InvolvedObject.Kind), event.InvolvedObject.Name, event.InvolvedObject.Namespace),
Tags: sfields,
}
opts := []postOption{
withRequestModifier(func(req *retryablehttp.Request) {
if (g.Username != "" && g.Password != "") && g.Token == "" {
req.Header.Add("Authorization", "Basic "+basicAuth(g.Username, g.Password))
}
if g.Token != "" {
req.Header.Add("Authorization", "Bearer "+g.Token)
}
}),
}
if g.ProxyURL != "" {
opts = append(opts, withProxy(g.ProxyURL))
}
if g.TLSConfig != nil {
opts = append(opts, withTLSConfig(g.TLSConfig))
}
if err := postMessage(ctx, g.URL, payload, opts...); err != nil {
err := postMessage(ctx, g.URL, g.ProxyURL, g.CertPool, payload, func(request *retryablehttp.Request) {
if (g.Username != "" && g.Password != "") && g.Token == "" {
request.Header.Add("Authorization", "Basic "+basicAuth(g.Username, g.Password))
}
if g.Token != "" {
request.Header.Add("Authorization", "Bearer "+g.Token)
}
})
if err != nil {
return fmt.Errorf("postMessage failed: %w", err)
}
return nil

View File

@ -18,7 +18,7 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
@ -43,10 +43,10 @@ func Fuzz_Grafana(f *testing.F) {
}))
defer ts.Close()
var tlsConfig tls.Config
_ = fuzz.NewConsumer(seed).GenerateStruct(&tlsConfig)
var cert x509.CertPool
_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
grafana, err := NewGrafana(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", token, &tlsConfig, username, password)
grafana, err := NewGrafana(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), "", token, &cert, username, password)
if err != nil {
return
}

View File

@ -41,9 +41,6 @@ func TestGrafana_Post(t *testing.T) {
require.Equal(t, "flux", payload.Tags[0])
require.Equal(t, "source-controller", payload.Tags[1])
require.Equal(t, "test: metadata", payload.Tags[2])
require.Equal(t, "kind: GitRepository", payload.Tags[3])
require.Equal(t, "name: webapp", payload.Tags[4])
require.Equal(t, "namespace: gitops-system", payload.Tags[5])
}))
defer ts.Close()

View File

@ -109,5 +109,5 @@ func (l *Lark) Post(ctx context.Context, event eventv1.Event) error {
Card: card,
}
return postMessage(ctx, l.URL, payload)
return postMessage(ctx, l.URL, "", nil, payload)
}

View File

@ -3,7 +3,7 @@ package notifier
import (
"context"
"crypto/sha1"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
@ -15,10 +15,10 @@ import (
)
type Matrix struct {
Token string
URL string
RoomId string
TLSConfig *tls.Config
Token string
URL string
RoomId string
CertPool *x509.CertPool
}
type MatrixPayload struct {
@ -26,17 +26,17 @@ type MatrixPayload struct {
MsgType string `json:"msgtype"`
}
func NewMatrix(serverURL, token, roomId string, tlsConfig *tls.Config) (*Matrix, error) {
func NewMatrix(serverURL, token, roomId string, certPool *x509.CertPool) (*Matrix, error) {
_, err := url.ParseRequestURI(serverURL)
if err != nil {
return nil, fmt.Errorf("invalid Matrix homeserver URL %s: '%w'", serverURL, err)
}
return &Matrix{
URL: serverURL,
RoomId: roomId,
Token: token,
TLSConfig: tlsConfig,
URL: serverURL,
RoomId: roomId,
Token: token,
CertPool: certPool,
}, nil
}
@ -65,17 +65,11 @@ func (m *Matrix) Post(ctx context.Context, event eventv1.Event) error {
MsgType: "m.text",
}
opts := []postOption{
withRequestModifier(func(req *retryablehttp.Request) {
req.Method = http.MethodPut
req.Header.Add("Authorization", "Bearer "+m.Token)
}),
}
if m.TLSConfig != nil {
opts = append(opts, withTLSConfig(m.TLSConfig))
}
if err := postMessage(ctx, fullURL, payload, opts...); err != nil {
err = postMessage(ctx, fullURL, "", m.CertPool, payload, func(request *retryablehttp.Request) {
request.Method = http.MethodPut
request.Header.Add("Authorization", "Bearer "+m.Token)
})
if err != nil {
return fmt.Errorf("postMessage failed: %w", err)
}

View File

@ -2,7 +2,7 @@ package notifier
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
@ -27,10 +27,10 @@ func Fuzz_Matrix(f *testing.F) {
}))
defer ts.Close()
var tlsConfig tls.Config
_ = fuzz.NewConsumer(seed).GenerateStruct(&tlsConfig)
var cert x509.CertPool
_ = fuzz.NewConsumer(seed).GenerateStruct(&cert)
matrix, err := NewMatrix(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, roomId, &tlsConfig)
matrix, err := NewMatrix(fmt.Sprintf("%s/%s", ts.URL, urlSuffix), token, roomId, &cert)
if err != nil {
return
}

29
internal/notifier/nop.go Normal file
View File

@ -0,0 +1,29 @@
/*
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.
*/
package notifier
import (
"context"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
)
type NopNotifier struct{}
func (n *NopNotifier) Post(ctx context.Context, event eventv1.Event) error {
return nil
}

View File

@ -22,9 +22,6 @@ import (
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
)
// OperationPost is the operation name used in cache event metrics
const OperationPost = "post"
type Interface interface {
Post(ctx context.Context, event eventv1.Event) error
}

Some files were not shown because too many files have changed in this diff Show More