mirror of https://github.com/fluxcd/pkg.git
Compare commits
104 Commits
runtime/v0
...
main
Author | SHA1 | Date |
---|---|---|
|
4ca8fb011f | |
|
8ce7a0dc67 | |
|
a254eaccd7 | |
|
6bf77f094c | |
|
b0abad5db0 | |
|
621a899e4c | |
|
2c7a66601d | |
|
4bb23ecbdb | |
|
d6fbf47b79 | |
|
a849bcf3b7 | |
|
a795fdc737 | |
|
11982f6ab4 | |
|
a2c5712e16 | |
|
48209adba5 | |
|
f227e67fdd | |
|
5f5e254bd2 | |
|
c3282981b7 | |
|
f01e8d6848 | |
|
e77a11bc22 | |
|
9fd9628968 | |
|
04d916d0c5 | |
|
4642dabf28 | |
|
27b414e80f | |
|
dbf1d227b0 | |
|
cb022f764d | |
|
488ca0955a | |
|
1c8c7bb531 | |
|
e98aecf00e | |
|
b59d18da0a | |
|
44b53bd4ae | |
|
ff888a4ac7 | |
|
95760a79c9 | |
|
60b1dea324 | |
|
7930b7b806 | |
|
ea0856db0e | |
|
525798301f | |
|
4f64822f3a | |
|
a75690c399 | |
|
447804609f | |
|
754624dafa | |
|
d93949611e | |
|
ef45e8d331 | |
|
c3b108fc36 | |
|
fddd5f143b | |
|
22e822f7d9 | |
|
dc9bf746cb | |
|
8804d2f561 | |
|
269920c337 | |
|
2e38bdab2d | |
|
7528b2cc9c | |
|
86263bb10b | |
|
7c029aeb7b | |
|
7a820924bf | |
|
252071547d | |
|
200b6b6972 | |
|
237b27666d | |
|
c706ea8774 | |
|
f63a79748e | |
|
578210e99f | |
|
0a9d0bb2a8 | |
|
c866cff616 | |
|
72ea7ac97f | |
|
66a0184e4d | |
|
e9a43c60b3 | |
|
6d798f4353 | |
|
8bef64b166 | |
|
97cc6ae9e4 | |
|
e4649aeb4c | |
|
5d6f266ea7 | |
|
5aaefdfd83 | |
|
7bbf65e375 | |
|
d41b5b4983 | |
|
cd4fe98d56 | |
|
d63225ec17 | |
|
b58975195e | |
|
93ecf129b5 | |
|
f30f3eb1dc | |
|
da8d033dfc | |
|
a3804267aa | |
|
6d156d81e1 | |
|
022e5483ca | |
|
ddfc64b0ad | |
|
18c2741e48 | |
|
856b04da5d | |
|
14d994dcd5 | |
|
c929dfc8af | |
|
9e79277372 | |
|
9ee2d52b4b | |
|
e04986eb42 | |
|
59d65f6222 | |
|
607f4b2d84 | |
|
c895e9f68f | |
|
6c8e4c1892 | |
|
1649a3c36d | |
|
07494843c9 | |
|
647e4790e3 | |
|
53191111ba | |
|
1a2d666360 | |
|
534b608a89 | |
|
69c9e52ecd | |
|
2f61fd67e3 | |
|
14aaf932a7 | |
|
24b87dc71e | |
|
c94a1a436a |
|
@ -21,7 +21,7 @@ jobs:
|
|||
name: actions on ${{ matrix.version }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup yq
|
||||
uses: ./actions/yq
|
||||
- name: Setup kubeconform
|
||||
|
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
|
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
|
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
- github
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
# Since this is a monorepo, changes in other packages will also trigger these e2e tests
|
||||
# meant only for the git package. This detects us whether the changed files are part of the
|
||||
|
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
working-directory: ./oci/tests/integration
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
|
@ -35,7 +35,7 @@ jobs:
|
|||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||
- name: configure aws credentials
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.OCI_E2E_AWS_ASSUME_ROLE_NAME }}
|
||||
role-session-name: OCI_GH_Actions
|
||||
|
@ -43,7 +43,7 @@ jobs:
|
|||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
- name: Set dynamic variables in .env
|
||||
run: |
|
||||
cat > .env <<EOF
|
||||
|
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
working-directory: ./oci/tests/integration
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
|
@ -40,7 +40,7 @@ jobs:
|
|||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
- name: Set dynamic variables in .env
|
||||
run: |
|
||||
cat > .env <<EOF
|
||||
|
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
working-directory: ./tools/reaper
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
repository: fluxcd/test-infra
|
||||
- name: Setup Go
|
||||
|
@ -35,7 +35,7 @@ jobs:
|
|||
run: echo "GCRGC_VERSION=${GCRGC_VERSION}" >> $GITHUB_ENV
|
||||
- name: Cache gcrgc
|
||||
id: cache-gcrgc
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ~/.local/bin/gcrgc
|
||||
key: gcrgc-${{ env.GCRGC_VERSION }}
|
||||
|
@ -46,11 +46,11 @@ jobs:
|
|||
wget https://github.com/graillus/gcrgc/releases/download/v${GCRGC_VERSION}/gcrgc_${GCRGC_VERSION}_linux_amd64.tar.gz -O - | tar xz
|
||||
mv gcrgc ~/.local/bin/
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193 # v2.1.10
|
||||
uses: google-github-actions/auth@b7593ed2efd1c1617e1b0254da33b86225adb2a5 # v2.1.12
|
||||
with:
|
||||
credentials_json: '${{ secrets.CLEANUP_E2E_GOOGLE_CREDENTIALS }}'
|
||||
- name: Setup gcloud
|
||||
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4
|
||||
uses: google-github-actions/setup-gcloud@cb1e50a9932213ecece00a606661ae9ca44f3397 # v2.2.0
|
||||
- name: Run gcrgc
|
||||
# Cleanup all the GCR repositories in the project. They are not tracked
|
||||
# by terraform used to provision test infra and are left behind.
|
||||
|
@ -66,7 +66,7 @@ jobs:
|
|||
if: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
repository: fluxcd/test-infra
|
||||
- name: Setup Go
|
||||
|
@ -89,7 +89,7 @@ jobs:
|
|||
if: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
repository: fluxcd/test-infra
|
||||
- name: Setup Go
|
||||
|
@ -98,7 +98,7 @@ jobs:
|
|||
go-version: 1.24.x
|
||||
cache-dependency-path: ./tools/reaper/go.sum
|
||||
- name: Authenticate to AWS
|
||||
uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
|
||||
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.CLEANUP_E2E_AWS_ASSUME_ROLE_NAME }}
|
||||
role-session-name: cleanup_GH_Actions
|
||||
|
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
working-directory: ./oci/tests/integration
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
|
@ -34,25 +34,25 @@ jobs:
|
|||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193 # v2.1.10
|
||||
uses: google-github-actions/auth@b7593ed2efd1c1617e1b0254da33b86225adb2a5 # v2.1.12
|
||||
id: 'auth'
|
||||
with:
|
||||
credentials_json: '${{ secrets.OCI_E2E_GOOGLE_CREDENTIALS }}'
|
||||
token_format: 'access_token'
|
||||
- name: Setup gcloud
|
||||
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4
|
||||
uses: google-github-actions/setup-gcloud@cb1e50a9932213ecece00a606661ae9ca44f3397 # v2.2.0
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
- name: Log into gcr.io
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: gcr.io
|
||||
username: oauth2accesstoken
|
||||
password: ${{ steps.auth.outputs.access_token }}
|
||||
- name: Log into us-central1-docker.pkg.dev
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: us-central1-docker.pkg.dev
|
||||
username: oauth2accesstoken
|
||||
|
|
|
@ -25,9 +25,9 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
|
||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
|
@ -50,6 +50,6 @@ jobs:
|
|||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.8
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
name: preview-release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
preview-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
- run: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- run: git config --global user.name "github-actions[bot]"
|
||||
- run: make tools
|
||||
- run: ./bin/flux-tools pkg prep --yes
|
||||
- run: git add .
|
||||
- run: git commit -m "Release preview" || true
|
||||
- run: ./bin/flux-tools pkg release --preview
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
with:
|
||||
|
@ -28,13 +28,13 @@ jobs:
|
|||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
uses: github/codeql-action/init@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.8
|
||||
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@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
uses: github/codeql-action/autobuild@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.8
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||
uses: github/codeql-action/analyze@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.8
|
||||
|
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3
|
||||
with:
|
||||
# Configuration file
|
||||
|
|
19
Makefile
19
Makefile
|
@ -119,3 +119,22 @@ fuzz-smoketest: fuzz-build
|
|||
-e ENVTEST_BIN_VERSION=$(ENVTEST_KUBERNETES_VERSION) \
|
||||
local-fuzzing:latest \
|
||||
bash -c "/runner.sh"
|
||||
|
||||
# Prepare release for Go modules.
|
||||
.PHONY: prep
|
||||
prep: tools
|
||||
@./bin/flux-tools pkg prep
|
||||
|
||||
# Release Go modules.
|
||||
.PHONY: release
|
||||
release: tools
|
||||
@./bin/flux-tools pkg release
|
||||
|
||||
# Run vet for tools.
|
||||
.PHONY: tools
|
||||
tools:
|
||||
@cd cmd; \
|
||||
go mod tidy; \
|
||||
go fmt ./internal/... ./cli/...; \
|
||||
go vet ./internal/... ./cli/...; \
|
||||
go build -o ../bin/flux-tools ./cli
|
||||
|
|
61
README.md
61
README.md
|
@ -3,4 +3,63 @@
|
|||
[](https://pkg.go.dev/github.com/fluxcd/pkg)
|
||||
[](https://github.com/fluxcd/pkg/actions)
|
||||
|
||||
GitOps Toolkit common packages.
|
||||
## GitOps Toolkit Go SDK
|
||||
|
||||
### APIs
|
||||
- **[github.com/fluxcd/pkg/apis/acl](./apis/acl)** - API types for defining access control lists
|
||||
- **[github.com/fluxcd/pkg/apis/event](./apis/event)** - API Schema definitions for Flux eventing
|
||||
- **[github.com/fluxcd/pkg/apis/kustomize](./apis/kustomize)** - API types for Kustomize resources
|
||||
- **[github.com/fluxcd/pkg/apis/meta](./apis/meta)** - Generic metadata APIs for Kubernetes resources
|
||||
|
||||
### Authentication & Security
|
||||
- **[github.com/fluxcd/pkg/auth](./auth)** - OIDC-based authentication with cloud providers (AWS, Azure, GCP)
|
||||
- **[github.com/fluxcd/pkg/masktoken](./masktoken)** - Token redaction utilities for secure logging
|
||||
- **[github.com/fluxcd/pkg/ssh](./ssh)** - SSH host key scanning and management
|
||||
|
||||
### Controller Runtime
|
||||
- **[github.com/fluxcd/pkg/runtime](./runtime)** - Controller Runtime SDK
|
||||
- **[runtime/acl](./runtime/acl)** - Cross-namespace access control utilities
|
||||
- **[runtime/cel](./runtime/cel)** - Common Expression Language (CEL) evaluation utilities
|
||||
- **[runtime/client](./runtime/client)** - Kubernetes client runtime configuration options
|
||||
- **[runtime/conditions](./runtime/conditions)** - Status conditions manipulation utilities
|
||||
- **[runtime/controller](./runtime/controller)** - Controller embeddable structs for GitOps Toolkit conventions
|
||||
- **[runtime/dependency](./runtime/dependency)** - Dependency sorting for Kubernetes resources
|
||||
- **[runtime/errors](./runtime/errors)** - Generic controller and reconciler runtime errors
|
||||
- **[runtime/events](./runtime/events)** - Kubernetes Events recording on external HTTP endpoints
|
||||
- **[runtime/features](./runtime/features)** - Feature gate management
|
||||
- **[runtime/jitter](./runtime/jitter)** - Jitter utilities for reconciliation intervals
|
||||
- **[runtime/leaderelection](./runtime/leaderelection)** - Leader election runtime configuration
|
||||
- **[runtime/logger](./runtime/logger)** - Logging runtime configuration options
|
||||
- **[runtime/metrics](./runtime/metrics)** - Standard metrics recording for GitOps Toolkit components
|
||||
- **[runtime/object](./runtime/object)** - Helpers for interacting with GitOps Toolkit objects
|
||||
- **[runtime/patch](./runtime/patch)** - Patch utilities for conflict-free object patching
|
||||
- **[runtime/pprof](./runtime/pprof)** - pprof endpoints registration helper
|
||||
- **[runtime/predicates](./runtime/predicates)** - Controller-runtime predicates for event filtering
|
||||
- **[runtime/probes](./runtime/probes)** - Health and readiness probes configuration
|
||||
- **[runtime/reconcile](./runtime/reconcile)** - Reconciliation helpers and result finalization
|
||||
- **[runtime/secrets](./runtime/secrets)** - Kubernetes secrets handling utilities (TLS, auth, tokens)
|
||||
- **[runtime/statusreaders](./runtime/statusreaders)** - Status readers for Kubernetes resources
|
||||
- **[runtime/testenv](./runtime/testenv)** - Setup helpers for local Kubernetes test environment
|
||||
- **[runtime/transform](./runtime/transform)** - Type transformation utilities
|
||||
- **[github.com/fluxcd/pkg/ssa](./ssa)** - Kubernetes resources management using server-side apply
|
||||
|
||||
### Source Management
|
||||
- **[github.com/fluxcd/pkg/git](./git)** - Git repository operations, commit verification, and reference handling
|
||||
- **[github.com/fluxcd/pkg/sourceignore](./sourceignore)** - Gitignore-like functionality for source filtering
|
||||
|
||||
### Package Management
|
||||
- **[github.com/fluxcd/pkg/chartutil](./chartutil)** - Helm chart values management from Kubernetes resources
|
||||
- **[github.com/fluxcd/pkg/kustomize](./kustomize)** - Generic helpers for Kustomize operations
|
||||
- **[github.com/fluxcd/pkg/oci](./oci)** - OCI registry operations (push, pull, tag artifacts)
|
||||
|
||||
### Utilities
|
||||
- **[github.com/fluxcd/pkg/cache](./cache)** - Generic cache implementations (expiring and LRU)
|
||||
- **[github.com/fluxcd/pkg/envsubst](./envsubst)** - Variable expansion in strings using `${var}` syntax
|
||||
- **[github.com/fluxcd/pkg/lockedfile](./lockedfile)** - Atomic file operations with locking
|
||||
- **[github.com/fluxcd/pkg/tar](./tar)** - Secure tarball extraction utilities
|
||||
- **[github.com/fluxcd/pkg/version](./version)** - Semantic version parsing and sorting
|
||||
|
||||
### HTTP & Transport
|
||||
- **[github.com/fluxcd/pkg/http/fetch](./http/fetch)** - Archive fetcher for HTTP resources
|
||||
- **[github.com/fluxcd/pkg/http/transport](./http/transport)** - HTTP transport utilities
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ module github.com/fluxcd/pkg/apis/event
|
|||
go 1.24.0
|
||||
|
||||
require (
|
||||
k8s.io/api v0.33.0
|
||||
k8s.io/apimachinery v0.33.0
|
||||
k8s.io/api v0.33.2
|
||||
k8s.io/apimachinery v0.33.2
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -16,13 +16,14 @@ require (
|
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/text v0.24.0 // 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
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
|
||||
sigs.k8s.io/yaml v1.5.0 // indirect
|
||||
)
|
||||
|
|
|
@ -39,6 +39,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
|||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.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=
|
||||
|
@ -48,8 +50,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
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=
|
||||
|
@ -58,8 +60,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
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=
|
||||
|
@ -75,10 +77,10 @@ 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.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.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
||||
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
|
||||
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
|
||||
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-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
|
@ -88,7 +90,8 @@ sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8
|
|||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/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=
|
||||
|
|
|
@ -2,7 +2,7 @@ module github.com/fluxcd/pkg/apis/kustomize
|
|||
|
||||
go 1.24.0
|
||||
|
||||
require k8s.io/apiextensions-apiserver v0.33.0
|
||||
require k8s.io/apiextensions-apiserver v0.33.2
|
||||
|
||||
require (
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
|
@ -13,15 +13,16 @@ require (
|
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/text v0.23.0 // 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
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
k8s.io/apimachinery v0.33.0 // indirect
|
||||
k8s.io/apimachinery v0.33.2 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
|
||||
sigs.k8s.io/yaml v1.5.0 // indirect
|
||||
)
|
||||
|
|
|
@ -39,6 +39,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
|||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.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=
|
||||
|
@ -48,8 +50,8 @@ 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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
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/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=
|
||||
|
@ -58,8 +60,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
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=
|
||||
|
@ -75,10 +77,10 @@ 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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
|
||||
k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
|
||||
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
||||
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8=
|
||||
k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8=
|
||||
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-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
|
@ -88,7 +90,8 @@ sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8
|
|||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/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=
|
||||
|
|
|
@ -2,7 +2,7 @@ module github.com/fluxcd/pkg/apis/meta
|
|||
|
||||
go 1.24.0
|
||||
|
||||
require k8s.io/apimachinery v0.33.0
|
||||
require k8s.io/apimachinery v0.33.2
|
||||
|
||||
require (
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
|
@ -12,13 +12,14 @@ require (
|
|||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/text v0.24.0 // 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
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
|
||||
sigs.k8s.io/yaml v1.5.0 // indirect
|
||||
)
|
||||
|
|
|
@ -32,6 +32,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
|||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.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=
|
||||
|
@ -41,8 +43,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
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=
|
||||
|
@ -51,8 +53,8 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
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=
|
||||
|
@ -67,8 +69,8 @@ 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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
||||
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
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-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
|
@ -78,7 +80,8 @@ sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8
|
|||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/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=
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
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 meta
|
||||
|
||||
const (
|
||||
// LabelKeyWatch is used to indicate that a resource should be watched by Flux.
|
||||
LabelKeyWatch = "reconcile.fluxcd.io/watch"
|
||||
|
||||
// LabelValueWatchEnabled is the value for LabelKeyWatch that indicates a resource should be watched.
|
||||
LabelValueWatchEnabled = "Enabled"
|
||||
)
|
|
@ -35,6 +35,14 @@ type NamespacedObjectReference struct {
|
|||
Namespace string `json:"namespace,omitempty"`
|
||||
}
|
||||
|
||||
// String implements the fmt.Stringer interface for NamespacedObjectReference.
|
||||
func (in NamespacedObjectReference) String() string {
|
||||
if in.Namespace != "" {
|
||||
return in.Namespace + "/" + in.Name
|
||||
}
|
||||
return in.Name
|
||||
}
|
||||
|
||||
// NamespacedObjectKindReference contains enough information to locate the typed referenced Kubernetes resource object
|
||||
// in any namespace.
|
||||
type NamespacedObjectKindReference struct {
|
||||
|
@ -69,19 +77,75 @@ type SecretKeyReference struct {
|
|||
Key string `json:"key,omitempty"`
|
||||
}
|
||||
|
||||
// KubeConfigReference contains enough information to locate the referenced
|
||||
// Kubernetes secret that contains a kubeconfig file.
|
||||
const (
|
||||
// KubeConfigKeyProvider is the key in the ConfigMap that contains the provider name.
|
||||
KubeConfigKeyProvider = "provider"
|
||||
// KubeConfigKeyAddress is the key in the ConfigMap that contains the cluster resource
|
||||
// name in the provider API
|
||||
KubeConfigKeyCluster = "cluster"
|
||||
// KubeConfigKeyAddress is the key in the ConfigMap that contains the address of the
|
||||
// Kubernetes API server.
|
||||
KubeConfigKeyAddress = "address"
|
||||
// KubeConfigKeyCACert is the key in the ConfigMap that contains the PEM-encoded CA
|
||||
// certificate for the Kubernetes API server.
|
||||
KubeConfigKeyCACert = "ca.crt"
|
||||
// KubeConfigKeyAudiences is the key in the ConfigMap that contains the audiences
|
||||
// for the Kubernetes ServiceAccount token.
|
||||
KubeConfigKeyAudiences = "audiences"
|
||||
// KubeConfigKeyServiceAccountName is the key in the ConfigMap that contains the
|
||||
// name of the Kubernetes ServiceAccount in the same namespace that should be used
|
||||
// for authentication.
|
||||
KubeConfigKeyServiceAccountName = "serviceAccountName"
|
||||
)
|
||||
|
||||
// KubeConfigReference contains enough information build a kubeconfig
|
||||
// in memory for connecting to remote Kubernetes clusters.
|
||||
// +kubebuilder:validation:XValidation:rule="has(self.configMapRef) || has(self.secretRef)", message="exactly one of spec.kubeConfig.configMapRef or spec.kubeConfig.secretRef must be specified"
|
||||
// +kubebuilder:validation:XValidation:rule="!has(self.configMapRef) || !has(self.secretRef)", message="exactly one of spec.kubeConfig.configMapRef or spec.kubeConfig.secretRef must be specified"
|
||||
type KubeConfigReference struct {
|
||||
// SecretRef holds the name of a secret that contains a key with
|
||||
// ConfigMapRef holds an optional name of a ConfigMap that contains
|
||||
// the following keys:
|
||||
//
|
||||
// - `provider`: the provider to use. One of `aws`, `azure`, `gcp`, or
|
||||
// `generic`. Required.
|
||||
// - `cluster`: the fully qualified resource name of the Kubernetes
|
||||
// cluster in the cloud provider API. Not used by the `generic`
|
||||
// provider. Required when one of `address` or `ca.crt` is not set.
|
||||
// - `address`: the address of the Kubernetes API server. Required
|
||||
// for `generic`. For the other providers, if not specified, the
|
||||
// first address in the cluster resource will be used, and if
|
||||
// specified, it must match one of the addresses in the cluster
|
||||
// resource.
|
||||
// If audiences is not set, will be used as the audience for the
|
||||
// `generic` provider.
|
||||
// - `ca.crt`: the optional PEM-encoded CA certificate for the
|
||||
// Kubernetes API server. If not set, the controller will use the
|
||||
// CA certificate from the cluster resource.
|
||||
// - `audiences`: the optional audiences as a list of
|
||||
// line-break-separated strings for the Kubernetes ServiceAccount
|
||||
// token. Defaults to the `address` for the `generic` provider, or
|
||||
// to specific values for the other providers depending on the
|
||||
// provider.
|
||||
// - `serviceAccountName`: the optional name of the Kubernetes
|
||||
// ServiceAccount in the same namespace that should be used
|
||||
// for authentication. If not specified, the controller
|
||||
// ServiceAccount will be used.
|
||||
//
|
||||
// Mutually exclusive with SecretRef.
|
||||
//
|
||||
// +optional
|
||||
ConfigMapRef *LocalObjectReference `json:"configMapRef,omitempty"`
|
||||
|
||||
// SecretRef holds an optional name of a secret that contains a key with
|
||||
// the kubeconfig file as the value. If no key is set, the key will default
|
||||
// to 'value'.
|
||||
// to 'value'. Mutually exclusive with ConfigMapRef.
|
||||
// It is recommended that the kubeconfig is self-contained, and the secret
|
||||
// is regularly updated if credentials such as a cloud-access-token expire.
|
||||
// Cloud specific `cmd-path` auth helpers will not function without adding
|
||||
// binaries and credentials to the Pod that is responsible for reconciling
|
||||
// Kubernetes resources.
|
||||
// +required
|
||||
SecretRef SecretKeyReference `json:"secretRef"`
|
||||
// Kubernetes resources. Supported only for the generic provider.
|
||||
// +optional
|
||||
SecretRef *SecretKeyReference `json:"secretRef,omitempty"`
|
||||
}
|
||||
|
||||
// ValuesReference contains a reference to a resource containing Helm values,
|
||||
|
|
|
@ -40,7 +40,16 @@ func (in *ForceRequestStatus) DeepCopy() *ForceRequestStatus {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *KubeConfigReference) DeepCopyInto(out *KubeConfigReference) {
|
||||
*out = *in
|
||||
out.SecretRef = in.SecretRef
|
||||
if in.ConfigMapRef != nil {
|
||||
in, out := &in.ConfigMapRef, &out.ConfigMapRef
|
||||
*out = new(LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.SecretRef != nil {
|
||||
in, out := &in.SecretRef, &out.SecretRef
|
||||
*out = new(SecretKeyReference)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeConfigReference.
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
authnv1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/fluxcd/pkg/cache"
|
||||
)
|
||||
|
||||
// GetAccessToken returns an access token for accessing resources in the given cloud provider.
|
||||
func GetAccessToken(ctx context.Context, provider Provider, opts ...Option) (Token, error) {
|
||||
|
||||
var o Options
|
||||
o.Apply(opts...)
|
||||
|
||||
// Initialize access token fetcher for controller.
|
||||
newAccessToken := func() (Token, error) {
|
||||
token, err := provider.NewControllerToken(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create provider access token for the controller: %w", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Update access token fetcher for a service account if specified.
|
||||
var serviceAccount *corev1.ServiceAccount
|
||||
var providerIdentity string
|
||||
var audiences []string
|
||||
if o.ServiceAccount != nil {
|
||||
// Fetch service account details.
|
||||
var err error
|
||||
serviceAccount, audiences, providerIdentity, err =
|
||||
getServiceAccountAndProviderInfo(ctx, provider, o.Client, *o.ServiceAccount, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update the function to create an access token using the service account.
|
||||
newAccessToken = func() (Token, error) {
|
||||
// Check the feature gate for object-level workload identity.
|
||||
if !IsObjectLevelWorkloadIdentityEnabled() {
|
||||
return nil, ErrObjectLevelWorkloadIdentityNotEnabled
|
||||
}
|
||||
|
||||
// Issue Kubernetes OIDC token for the service account.
|
||||
tokenReq := &authnv1.TokenRequest{
|
||||
Spec: authnv1.TokenRequestSpec{
|
||||
Audiences: audiences,
|
||||
},
|
||||
}
|
||||
if err := o.Client.SubResource("token").Create(ctx, serviceAccount, tokenReq); err != nil {
|
||||
return nil, fmt.Errorf("failed to create kubernetes token for service account '%s/%s': %w",
|
||||
serviceAccount.Namespace, serviceAccount.Name, err)
|
||||
}
|
||||
oidcToken := tokenReq.Status.Token
|
||||
|
||||
// Exchange the Kubernetes OIDC token for a provider access token.
|
||||
token, err := provider.NewTokenForServiceAccount(ctx, oidcToken, *serviceAccount, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create provider access token for service account '%s/%s': %w",
|
||||
serviceAccount.Namespace, serviceAccount.Name, err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Bail out early if cache is disabled.
|
||||
if o.Cache == nil {
|
||||
return newAccessToken()
|
||||
}
|
||||
|
||||
// Build cache key.
|
||||
cacheKey := buildAccessTokenCacheKey(provider, audiences, providerIdentity, serviceAccount, opts...)
|
||||
|
||||
// Build involved object details.
|
||||
kind := o.InvolvedObject.Kind
|
||||
name := o.InvolvedObject.Name
|
||||
namespace := o.InvolvedObject.Namespace
|
||||
operation := o.InvolvedObject.Operation
|
||||
|
||||
// Get token from cache.
|
||||
token, _, err := o.Cache.GetOrSet(ctx, cacheKey, func(ctx context.Context) (cache.Token, error) {
|
||||
return newAccessToken()
|
||||
}, cache.WithInvolvedObject(kind, name, namespace, operation))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func getServiceAccountAndProviderInfo(ctx context.Context, provider Provider, client client.Client,
|
||||
key client.ObjectKey, opts ...Option) (*corev1.ServiceAccount, []string, string, error) {
|
||||
|
||||
var o Options
|
||||
o.Apply(opts...)
|
||||
|
||||
// Get service account.
|
||||
var serviceAccount corev1.ServiceAccount
|
||||
if err := client.Get(ctx, key, &serviceAccount); err != nil {
|
||||
return nil, nil, "", fmt.Errorf("failed to get service account '%s/%s': %w",
|
||||
key.Namespace, key.Name, err)
|
||||
}
|
||||
|
||||
// Get provider audience.
|
||||
audiences := o.Audiences
|
||||
if len(audiences) == 0 {
|
||||
var err error
|
||||
audiences, err = provider.GetAudiences(ctx, serviceAccount)
|
||||
if err != nil {
|
||||
return nil, nil, "", fmt.Errorf("failed to get provider audience: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get provider identity.
|
||||
providerIdentity, err := provider.GetIdentity(serviceAccount)
|
||||
if err != nil {
|
||||
return nil, nil, "", fmt.Errorf("failed to get provider identity from service account '%s/%s' annotations: %w",
|
||||
key.Namespace, key.Name, err)
|
||||
}
|
||||
|
||||
return &serviceAccount, audiences, providerIdentity, nil
|
||||
}
|
||||
|
||||
func buildAccessTokenCacheKey(provider Provider, audiences []string, providerIdentity string,
|
||||
serviceAccount *corev1.ServiceAccount, opts ...Option) string {
|
||||
|
||||
var o Options
|
||||
o.Apply(opts...)
|
||||
|
||||
var parts []string
|
||||
|
||||
parts = append(parts, fmt.Sprintf("provider=%s", provider.GetName()))
|
||||
|
||||
if serviceAccount != nil {
|
||||
parts = append(parts, fmt.Sprintf("providerIdentity=%s", providerIdentity))
|
||||
parts = append(parts, fmt.Sprintf("serviceAccountName=%s", serviceAccount.Name))
|
||||
parts = append(parts, fmt.Sprintf("serviceAccountNamespace=%s", serviceAccount.Namespace))
|
||||
parts = append(parts, fmt.Sprintf("serviceAccountTokenAudiences=%s", strings.Join(audiences, ",")))
|
||||
}
|
||||
|
||||
if len(o.Scopes) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("scopes=%s", strings.Join(o.Scopes, ",")))
|
||||
}
|
||||
|
||||
if o.STSRegion != "" {
|
||||
parts = append(parts, fmt.Sprintf("stsRegion=%s", o.STSRegion))
|
||||
}
|
||||
|
||||
if o.STSEndpoint != "" {
|
||||
parts = append(parts, fmt.Sprintf("stsEndpoint=%s", o.STSEndpoint))
|
||||
}
|
||||
|
||||
if o.ProxyURL != nil {
|
||||
parts = append(parts, fmt.Sprintf("proxyURL=%s", o.ProxyURL))
|
||||
}
|
||||
|
||||
if o.CAData != "" {
|
||||
parts = append(parts, fmt.Sprintf("caData=%s", o.CAData))
|
||||
}
|
||||
|
||||
return buildCacheKey(parts...)
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
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 auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
"github.com/fluxcd/pkg/cache"
|
||||
)
|
||||
|
||||
func TestGetAccessToken(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
kubeClient, oidcClient := newTestEnv(t, ctx)
|
||||
|
||||
// Create a default service account.
|
||||
defaultServiceAccount := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
err := kubeClient.Create(ctx, defaultServiceAccount)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
saRef := client.ObjectKey{
|
||||
Name: defaultServiceAccount.Name,
|
||||
Namespace: defaultServiceAccount.Namespace,
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
provider *mockProvider
|
||||
opts []auth.Option
|
||||
disableObjectLevel bool
|
||||
expectedToken auth.Token
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "controller access token",
|
||||
provider: &mockProvider{
|
||||
returnControllerToken: &mockToken{token: "mock-default-token"},
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
},
|
||||
expectedToken: &mockToken{token: "mock-default-token"},
|
||||
},
|
||||
{
|
||||
name: "controller access token allowing shell out",
|
||||
provider: &mockProvider{
|
||||
returnControllerToken: &mockToken{token: "mock-default-token"},
|
||||
paramAllowShellOut: true,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
auth.WithAllowShellOut(),
|
||||
},
|
||||
expectedToken: &mockToken{token: "mock-default-token"},
|
||||
},
|
||||
{
|
||||
name: "access token from service account",
|
||||
provider: &mockProvider{
|
||||
returnName: "mock-provider",
|
||||
returnAccessToken: &mockToken{token: "mock-access-token"},
|
||||
paramAudiences: []string{"audience1", "audience2"},
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
paramOIDCTokenClient: oidcClient,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
// Exercise the code path where a cache is set but no token is
|
||||
// available in the cache.
|
||||
func(o *auth.Options) {
|
||||
tokenCache, err := cache.NewTokenCache(1)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
o.Cache = tokenCache
|
||||
},
|
||||
},
|
||||
expectedToken: &mockToken{token: "mock-access-token"},
|
||||
},
|
||||
{
|
||||
name: "access token from service account - default audience",
|
||||
provider: &mockProvider{
|
||||
returnName: "mock-provider",
|
||||
returnAccessToken: &mockToken{token: "mock-access-token"},
|
||||
paramAudiences: []string{},
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
paramOIDCTokenClient: oidcClient,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
},
|
||||
expectedToken: &mockToken{token: "mock-access-token"},
|
||||
},
|
||||
{
|
||||
name: "all the options are taken into account in the cache key",
|
||||
provider: &mockProvider{
|
||||
returnName: "mock-provider",
|
||||
returnIdentity: "mock-identity",
|
||||
paramAudiences: []string{"audience1", "audience2"},
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
func(o *auth.Options) {
|
||||
tokenCache, err := cache.NewTokenCache(1)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
const key = "db625bd5a96dc48fcc100659c6db98857d1e0ceec930bbded0fdece14af4307c"
|
||||
token := &mockToken{token: "cached-token"}
|
||||
cachedToken, ok, err := tokenCache.GetOrSet(ctx, key, func(ctx context.Context) (cache.Token, error) {
|
||||
return token, nil
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(ok).To(BeFalse())
|
||||
g.Expect(cachedToken).To(Equal(token))
|
||||
|
||||
o.Cache = tokenCache
|
||||
},
|
||||
},
|
||||
expectedToken: &mockToken{token: "cached-token"},
|
||||
},
|
||||
{
|
||||
name: "error getting identity",
|
||||
provider: &mockProvider{
|
||||
returnIdentityErr: "mock error",
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
},
|
||||
expectedErr: "failed to get provider identity from service account 'default/default' annotations: mock error",
|
||||
},
|
||||
{
|
||||
name: "error getting identity using cache",
|
||||
provider: &mockProvider{
|
||||
returnIdentityErr: "mock error",
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
func(o *auth.Options) {
|
||||
tokenCache, err := cache.NewTokenCache(1)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
o.Cache = tokenCache
|
||||
},
|
||||
},
|
||||
expectedErr: "failed to get provider identity from service account 'default/default' annotations: mock error",
|
||||
},
|
||||
{
|
||||
name: "disable object level workload identity",
|
||||
provider: &mockProvider{
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
},
|
||||
disableObjectLevel: true,
|
||||
expectedErr: "ObjectLevelWorkloadIdentity feature gate is not enabled",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tt.provider.t = t
|
||||
|
||||
if !tt.disableObjectLevel {
|
||||
t.Setenv(auth.EnvVarEnableObjectLevelWorkloadIdentity, "true")
|
||||
}
|
||||
|
||||
token, err := auth.GetAccessToken(ctx, tt.provider, tt.opts...)
|
||||
|
||||
if tt.expectedErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(Equal(tt.expectedErr))
|
||||
g.Expect(token).To(BeNil())
|
||||
} else {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(token).To(Equal(tt.expectedToken))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -37,23 +37,24 @@ func NewCredentialsProvider(ctx context.Context, opts ...auth.Option) aws.Creden
|
|||
|
||||
// Retrieve implements aws.CredentialsProvider.
|
||||
// The context is ignored, use the constructor to set the context.
|
||||
// This is because some callers of the library pass context.Background()
|
||||
// when calling this method (e.g. SOPS), so to ensure we have a real
|
||||
// context we pass it in the constructor.
|
||||
// This is because the GCP abstraction does not receive a context
|
||||
// in the method arguments, so we unfortunately need to standardize
|
||||
// the behavior of all providers around this so the usage of this
|
||||
// library can be consistent regardless of the provider.
|
||||
func (c *credentialsProvider) Retrieve(context.Context) (aws.Credentials, error) {
|
||||
token, err := auth.GetToken(c.ctx, Provider{}, c.opts...)
|
||||
token, err := auth.GetAccessToken(c.ctx, Provider{}, c.opts...)
|
||||
if err != nil {
|
||||
return aws.Credentials{}, err
|
||||
}
|
||||
awsToken, ok := token.(*Token)
|
||||
awsCreds, ok := token.(*Credentials)
|
||||
if !ok {
|
||||
return aws.Credentials{}, fmt.Errorf("failed to cast token to AWS token: %T", token)
|
||||
}
|
||||
return aws.Credentials{
|
||||
AccessKeyID: *awsToken.AccessKeyId,
|
||||
SecretAccessKey: *awsToken.SecretAccessKey,
|
||||
SessionToken: *awsToken.SessionToken,
|
||||
Expires: *awsToken.Expiration,
|
||||
AccessKeyID: *awsCreds.AccessKeyId,
|
||||
SecretAccessKey: *awsCreds.SecretAccessKey,
|
||||
SessionToken: *awsCreds.SessionToken,
|
||||
Expires: *awsCreds.Expiration,
|
||||
CanExpire: true,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -20,9 +20,11 @@ import (
|
|||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
signerv4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecrpublic"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
|
@ -32,6 +34,8 @@ type Implementation interface {
|
|||
AssumeRoleWithWebIdentity(ctx context.Context, params *sts.AssumeRoleWithWebIdentityInput, options sts.Options) (*sts.AssumeRoleWithWebIdentityOutput, error)
|
||||
GetAuthorizationToken(ctx context.Context, cfg aws.Config) (any, error)
|
||||
GetPublicAuthorizationToken(ctx context.Context, cfg aws.Config) (any, error)
|
||||
DescribeCluster(ctx context.Context, params *eks.DescribeClusterInput, options eks.Options) (*eks.DescribeClusterOutput, error)
|
||||
PresignGetCallerIdentity(ctx context.Context, optFn func(*sts.PresignOptions), options sts.Options) (*signerv4.PresignedHTTPRequest, error)
|
||||
}
|
||||
|
||||
type implementation struct{}
|
||||
|
@ -51,3 +55,11 @@ func (implementation) GetAuthorizationToken(ctx context.Context, cfg aws.Config)
|
|||
func (implementation) GetPublicAuthorizationToken(ctx context.Context, cfg aws.Config) (any, error) {
|
||||
return ecrpublic.NewFromConfig(cfg).GetAuthorizationToken(ctx, &ecrpublic.GetAuthorizationTokenInput{})
|
||||
}
|
||||
|
||||
func (implementation) DescribeCluster(ctx context.Context, params *eks.DescribeClusterInput, options eks.Options) (*eks.DescribeClusterOutput, error) {
|
||||
return eks.New(options).DescribeCluster(ctx, params)
|
||||
}
|
||||
|
||||
func (implementation) PresignGetCallerIdentity(ctx context.Context, optFn func(*sts.PresignOptions), options sts.Options) (*signerv4.PresignedHTTPRequest, error) {
|
||||
return sts.NewPresignClient(sts.New(options)).PresignGetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}, optFn)
|
||||
}
|
||||
|
|
|
@ -19,16 +19,21 @@ package aws_test
|
|||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
signerv4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
ecrtypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecrpublic"
|
||||
ecrpublictypes "github.com/aws/aws-sdk-go-v2/service/ecrpublic/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks"
|
||||
ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
|
||||
. "github.com/onsi/gomega"
|
||||
|
@ -39,6 +44,8 @@ type mockImplementation struct {
|
|||
|
||||
publicECR bool
|
||||
|
||||
expectEKSAPICall bool
|
||||
|
||||
argRoleARN string
|
||||
argRoleSessionName string
|
||||
argOIDCToken string
|
||||
|
@ -46,10 +53,20 @@ type mockImplementation struct {
|
|||
argSTSEndpoint string
|
||||
argProxyURL *url.URL
|
||||
argCredsProvider aws.CredentialsProvider
|
||||
argClusterName string
|
||||
|
||||
returnCreds aws.Credentials
|
||||
returnUsername string
|
||||
returnPassword string
|
||||
returnCreds aws.Credentials
|
||||
returnUsername string
|
||||
returnPassword string
|
||||
returnEndpoint string
|
||||
returnCAData string
|
||||
returnPresignedURL string
|
||||
}
|
||||
|
||||
type mockHTTPPresigner struct {
|
||||
t *testing.T
|
||||
argClusterName string
|
||||
returnURL string
|
||||
}
|
||||
|
||||
type mockCredentialsProvider struct{ aws.Credentials }
|
||||
|
@ -131,6 +148,87 @@ func (m *mockImplementation) GetPublicAuthorizationToken(ctx context.Context, cf
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockImplementation) DescribeCluster(ctx context.Context, params *eks.DescribeClusterInput, options eks.Options) (*eks.DescribeClusterOutput, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(m.expectEKSAPICall).To(BeTrue())
|
||||
g.Expect(params).NotTo(BeNil())
|
||||
g.Expect(params.Name).NotTo(BeNil())
|
||||
g.Expect(*params.Name).To(Equal(m.argClusterName))
|
||||
g.Expect(options.Region).To(Equal(m.argRegion))
|
||||
g.Expect(options.Credentials).To(Equal(m.argCredsProvider))
|
||||
g.Expect(options.HTTPClient).NotTo(BeNil())
|
||||
g.Expect(options.HTTPClient.(*http.Client)).NotTo(BeNil())
|
||||
g.Expect(options.HTTPClient.(*http.Client).Transport).NotTo(BeNil())
|
||||
g.Expect(options.HTTPClient.(*http.Client).Transport.(*http.Transport)).NotTo(BeNil())
|
||||
g.Expect(options.HTTPClient.(*http.Client).Transport.(*http.Transport).Proxy).NotTo(BeNil())
|
||||
proxyURL, err := options.HTTPClient.(*http.Client).Transport.(*http.Transport).Proxy(nil)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(proxyURL).To(Equal(m.argProxyURL))
|
||||
return &eks.DescribeClusterOutput{
|
||||
Cluster: &ekstypes.Cluster{
|
||||
Name: aws.String(m.argClusterName),
|
||||
Endpoint: aws.String(m.returnEndpoint),
|
||||
CertificateAuthority: &ekstypes.Certificate{
|
||||
Data: aws.String(m.returnCAData),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockImplementation) PresignGetCallerIdentity(ctx context.Context, optFn func(*sts.PresignOptions), options sts.Options) (*signerv4.PresignedHTTPRequest, error) {
|
||||
m.t.Helper()
|
||||
|
||||
g := NewWithT(m.t)
|
||||
|
||||
// Check that optFn adds the presigner with the custom EKS headers to the options.
|
||||
g.Expect(optFn).NotTo(BeNil())
|
||||
mockPresigner := &mockHTTPPresigner{
|
||||
t: m.t,
|
||||
argClusterName: m.argClusterName,
|
||||
returnURL: m.returnPresignedURL,
|
||||
}
|
||||
var presignOpts sts.PresignOptions
|
||||
presignOpts.Presigner = mockPresigner
|
||||
optFn(&presignOpts)
|
||||
g.Expect(presignOpts.Presigner).NotTo(Equal(mockPresigner))
|
||||
req, _ := http.NewRequest("POST", "https://sts.amazonaws.com/", nil)
|
||||
signingTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
|
||||
signerOptFn := func(opts *signerv4.SignerOptions) { opts.LogSigning = true }
|
||||
creds := aws.Credentials{
|
||||
AccessKeyID: "access-key-id",
|
||||
SecretAccessKey: "secret-access-key",
|
||||
SessionToken: "session-token",
|
||||
}
|
||||
presignedURL, presignedHeader, err := presignOpts.Presigner.PresignHTTP(
|
||||
ctx, creds, req, "payload-hash", "sts", "us-east-1", signingTime, signerOptFn)
|
||||
g.Expect(presignedURL).To(Equal(m.returnPresignedURL))
|
||||
g.Expect(presignedHeader).To(Equal(http.Header{"foo": []string{"bar"}}))
|
||||
g.Expect(err).To(MatchError("mock presign error"))
|
||||
|
||||
// Check the sts options.
|
||||
g.Expect(options.Region).To(Equal(m.argRegion))
|
||||
g.Expect(options.Credentials).To(Equal(m.argCredsProvider))
|
||||
if m.argSTSEndpoint != "" {
|
||||
g.Expect(options.BaseEndpoint).NotTo(BeNil())
|
||||
g.Expect(*options.BaseEndpoint).To(Equal(m.argSTSEndpoint))
|
||||
} else {
|
||||
g.Expect(options.BaseEndpoint).To(BeNil())
|
||||
}
|
||||
g.Expect(options.HTTPClient).NotTo(BeNil())
|
||||
g.Expect(options.HTTPClient.(*http.Client)).NotTo(BeNil())
|
||||
g.Expect(options.HTTPClient.(*http.Client).Transport).NotTo(BeNil())
|
||||
g.Expect(options.HTTPClient.(*http.Client).Transport.(*http.Transport)).NotTo(BeNil())
|
||||
g.Expect(options.HTTPClient.(*http.Client).Transport.(*http.Transport).Proxy).NotTo(BeNil())
|
||||
proxyURL, err := options.HTTPClient.(*http.Client).Transport.(*http.Transport).Proxy(nil)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(proxyURL).To(Equal(m.argProxyURL))
|
||||
|
||||
return &signerv4.PresignedHTTPRequest{
|
||||
URL: m.returnPresignedURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockImplementation) checkGetAuthorizationToken(ctx context.Context, cfg aws.Config) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
|
@ -146,6 +244,38 @@ func (m *mockImplementation) checkGetAuthorizationToken(ctx context.Context, cfg
|
|||
g.Expect(proxyURL).To(Equal(m.argProxyURL))
|
||||
}
|
||||
|
||||
func (m *mockHTTPPresigner) PresignHTTP(ctx context.Context, credentials aws.Credentials,
|
||||
r *http.Request, payloadHash string, service string, region string, signingTime time.Time,
|
||||
optFns ...func(*signerv4.SignerOptions)) (url string, signedHeader http.Header, err error) {
|
||||
|
||||
m.t.Helper()
|
||||
|
||||
g := NewWithT(m.t)
|
||||
|
||||
// Check args.
|
||||
g.Expect(ctx).NotTo(BeNil())
|
||||
g.Expect(credentials.AccessKeyID).To(Equal("access-key-id"))
|
||||
g.Expect(credentials.SecretAccessKey).To(Equal("secret-access-key"))
|
||||
g.Expect(credentials.SessionToken).To(Equal("session-token"))
|
||||
g.Expect(r).NotTo(BeNil())
|
||||
g.Expect(r.Method).To(Equal("POST"))
|
||||
g.Expect(r.URL.String()).To(Equal("https://sts.amazonaws.com/"))
|
||||
g.Expect(r.Header.Get("x-k8s-aws-id")).To(Equal(m.argClusterName))
|
||||
g.Expect(r.Header.Get("X-Amz-Expires")).To(Equal("900"))
|
||||
g.Expect(payloadHash).To(Equal("payload-hash"))
|
||||
g.Expect(service).To(Equal("sts"))
|
||||
g.Expect(region).To(Equal("us-east-1"))
|
||||
g.Expect(signingTime).To(Equal(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)))
|
||||
g.Expect(optFns).To(HaveLen(1))
|
||||
optFn := optFns[0]
|
||||
g.Expect(optFn).NotTo(BeNil())
|
||||
var signerOpts signerv4.SignerOptions
|
||||
optFn(&signerOpts)
|
||||
g.Expect(signerOpts).To(Equal(signerv4.SignerOptions{LogSigning: true}))
|
||||
|
||||
return m.returnURL, http.Header{"foo": []string{"bar"}}, errors.New("mock presign error")
|
||||
}
|
||||
|
||||
func (m *mockCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
||||
return m.Credentials, nil
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ func ValidateSTSEndpoint(endpoint string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
const roleARNPattern = `^arn:aws:iam::[0-9]{1,30}:role/.{1,200}$`
|
||||
const roleARNPattern = `^arn:aws[\w-]*:iam::[0-9]{1,30}:role/.{1,200}$`
|
||||
|
||||
var roleARNRegex = regexp.MustCompile(roleARNPattern)
|
||||
|
||||
|
@ -64,3 +64,18 @@ func getRoleSessionName(serviceAccount corev1.ServiceAccount, region string) str
|
|||
namespace := serviceAccount.Namespace
|
||||
return fmt.Sprintf("%s.%s.%s.fluxcd.io", name, namespace, region)
|
||||
}
|
||||
|
||||
const clusterPattern = `^arn:aws[\w-]*:eks:([^:]{1,100}):[0-9]{1,30}:cluster/(.{1,200})$`
|
||||
|
||||
var clusterRegex = regexp.MustCompile(clusterPattern)
|
||||
|
||||
func parseCluster(cluster string) (string, string, error) {
|
||||
m := clusterRegex.FindStringSubmatch(cluster)
|
||||
if len(m) != 3 {
|
||||
return "", "", fmt.Errorf("invalid EKS cluster ARN: '%s'. must match %s",
|
||||
cluster, clusterPattern)
|
||||
}
|
||||
region := m[1]
|
||||
name := m[2]
|
||||
return region, name, nil
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import (
|
|||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
@ -31,6 +30,7 @@ import (
|
|||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecrpublic"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
@ -54,53 +54,32 @@ func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (
|
|||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
var awsOpts []func(*config.LoadOptions) error
|
||||
confOpts := []func(*config.LoadOptions) error{
|
||||
config.WithHTTPClient(o.GetHTTPClient()),
|
||||
}
|
||||
|
||||
stsRegion := o.STSRegion
|
||||
if stsRegion == "" {
|
||||
// A region is required. Try to get it somewhere else.
|
||||
switch {
|
||||
// For artifact repositories we can take advantage of the fact that ECR
|
||||
// repositories have a region we can use.
|
||||
// **Important**: This code path is required for supporting EKS Node Identity
|
||||
// for artifact repositories! This is because the environment variable
|
||||
// AWS_REGION is set automatically for IRSA or EKS Pod Identity, but
|
||||
// not for Node Identity.
|
||||
// We strive to support Node Identity for container registry-based APIs because
|
||||
// EKS users also use Node Identity for container images, so this allows a
|
||||
// simpler/consistent user experience.
|
||||
case o.ArtifactRepository != "":
|
||||
// We can safely ignore the error here, auth.GetToken() has already called
|
||||
// ParseArtifactRepository() and validated the repository at this point.
|
||||
registryInput, _ := p.ParseArtifactRepository(o.ArtifactRepository)
|
||||
stsRegion = getECRRegionFromRegistryInput(registryInput)
|
||||
// EKS sets this environment variable automatically if the controller pod is
|
||||
// properly configured with IRSA or EKS Pod Identity, so we can rely on this
|
||||
// and communicate this to users since this is controller-level configuration.
|
||||
default:
|
||||
stsRegion = os.Getenv("AWS_REGION")
|
||||
if stsRegion == "" {
|
||||
return nil, errors.New("AWS_REGION environment variable is not set in the Flux controller. " +
|
||||
"if you have properly configured IAM Roles for Service Accounts (IRSA) or EKS Pod Identity, " +
|
||||
"please delete/replace the controller pod so the EKS admission controllers can inject this " +
|
||||
"environment variable, or set it manually if the cluster is not EKS")
|
||||
}
|
||||
// properly configured with IRSA or EKS Pod Identity, so we can rely on it.
|
||||
stsRegion = os.Getenv("AWS_REGION")
|
||||
if stsRegion == "" {
|
||||
return nil, errors.New("AWS_REGION environment variable is not set in the Flux controller. " +
|
||||
"if you have properly configured IAM Roles for Service Accounts (IRSA) or EKS Pod Identity, " +
|
||||
"please delete/replace the controller pod so the EKS admission controllers can inject this " +
|
||||
"environment variable, or set it manually if the cluster is not EKS")
|
||||
}
|
||||
}
|
||||
awsOpts = append(awsOpts, config.WithRegion(stsRegion))
|
||||
confOpts = append(confOpts, config.WithRegion(stsRegion))
|
||||
|
||||
if e := o.STSEndpoint; e != "" {
|
||||
if err := ValidateSTSEndpoint(e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
awsOpts = append(awsOpts, config.WithBaseEndpoint(e))
|
||||
confOpts = append(confOpts, config.WithBaseEndpoint(e))
|
||||
}
|
||||
|
||||
if hc := o.GetHTTPClient(); hc != nil {
|
||||
awsOpts = append(awsOpts, config.WithHTTPClient(hc))
|
||||
}
|
||||
|
||||
conf, err := p.impl().LoadDefaultConfig(ctx, awsOpts...)
|
||||
conf, err := p.impl().LoadDefaultConfig(ctx, confOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -112,9 +91,9 @@ func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (
|
|||
return newTokenFromAWSCredentials(&creds), nil
|
||||
}
|
||||
|
||||
// GetAudience implements auth.Provider.
|
||||
func (Provider) GetAudience(context.Context, corev1.ServiceAccount) (string, error) {
|
||||
return "sts.amazonaws.com", nil
|
||||
// GetAudiences implements auth.Provider.
|
||||
func (Provider) GetAudiences(context.Context, corev1.ServiceAccount) ([]string, error) {
|
||||
return []string{"sts.amazonaws.com"}, nil
|
||||
}
|
||||
|
||||
// GetIdentity implements auth.Provider.
|
||||
|
@ -135,25 +114,14 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
|
|||
|
||||
stsRegion := o.STSRegion
|
||||
if stsRegion == "" {
|
||||
// A region is required. Try to get it somewhere else.
|
||||
switch {
|
||||
// For artifact repositories we can take advantage of the fact that ECR
|
||||
// repositories have a region we can use.
|
||||
case o.ArtifactRepository != "":
|
||||
// We can safely ignore the error here, auth.GetToken() has already called
|
||||
// ParseArtifactRepository() and validated the repository at this point.
|
||||
registryInput, _ := p.ParseArtifactRepository(o.ArtifactRepository)
|
||||
stsRegion = getECRRegionFromRegistryInput(registryInput)
|
||||
// In this case we can't rely on IRSA or EKS Pod Identity for the controller
|
||||
// pod because this is object-level configuration, so we show a different
|
||||
// error message.
|
||||
// In this error message we assume an API that has a region field, e.g. the
|
||||
// Bucket API. APIs that can extract the region from the ARN (e.g. KMS) will
|
||||
// never reach this code path.
|
||||
default:
|
||||
return nil, errors.New("an AWS region is required for authenticating with a service account. " +
|
||||
"please configure one in the object spec")
|
||||
}
|
||||
return nil, errors.New("an AWS region is required for authenticating with a service account. " +
|
||||
"please configure one in the object spec")
|
||||
}
|
||||
|
||||
roleARN, err := getRoleARN(serviceAccount)
|
||||
|
@ -163,22 +131,16 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
|
|||
|
||||
roleSessionName := getRoleSessionName(serviceAccount, stsRegion)
|
||||
|
||||
awsOpts := sts.Options{
|
||||
Region: stsRegion,
|
||||
stsOpts := sts.Options{
|
||||
Region: stsRegion,
|
||||
HTTPClient: o.GetHTTPClient(),
|
||||
}
|
||||
|
||||
if e := o.STSEndpoint; e != "" {
|
||||
if err := ValidateSTSEndpoint(e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
awsOpts.BaseEndpoint = &e
|
||||
}
|
||||
|
||||
if u := o.ProxyURL; u != nil {
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = http.ProxyURL(u)
|
||||
httpClient := &http.Client{Transport: transport}
|
||||
awsOpts.HTTPClient = httpClient
|
||||
stsOpts.BaseEndpoint = &e
|
||||
}
|
||||
|
||||
req := &sts.AssumeRoleWithWebIdentityInput{
|
||||
|
@ -186,7 +148,7 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
|
|||
RoleSessionName: &roleSessionName,
|
||||
WebIdentityToken: &oidcToken,
|
||||
}
|
||||
resp, err := p.impl().AssumeRoleWithWebIdentity(ctx, req, awsOpts)
|
||||
resp, err := p.impl().AssumeRoleWithWebIdentity(ctx, req, stsOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -194,12 +156,31 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
|
|||
return nil, fmt.Errorf("credentials are nil")
|
||||
}
|
||||
|
||||
token := &Token{*resp.Credentials}
|
||||
if token.Expiration == nil {
|
||||
token.Expiration = &time.Time{}
|
||||
creds := &Credentials{*resp.Credentials}
|
||||
if creds.Expiration == nil {
|
||||
creds.Expiration = &time.Time{}
|
||||
}
|
||||
|
||||
return token, nil
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
// GetAccessTokenOptionsForArtifactRepository implements auth.Provider.
|
||||
func (p Provider) GetAccessTokenOptionsForArtifactRepository(artifactRepository string) ([]auth.Option, error) {
|
||||
// AWS requires a region for getting access credentials. To avoid requiring
|
||||
// two regions to be passed in the Flux APIs we leverage the region present
|
||||
// in the ECR repository.
|
||||
// **Important**: This code path is required for supporting the identity of
|
||||
// the EKS node! The AWS_REGION environment variable is only automatically
|
||||
// set for IRSA and EKS Pod Identity. We strive to support the identity of
|
||||
// the node for artifact repository APIs because EKS users also use it for
|
||||
// for pulling container images to spin up pods inside the cluster, so this
|
||||
// allows a simpler user experience setting up ECR authentication only once.
|
||||
registryInput, err := p.ParseArtifactRepository(artifactRepository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ecrRegion := getECRRegionFromRegistryInput(registryInput)
|
||||
return []auth.Option{auth.WithSTSRegion(ecrRegion)}, nil
|
||||
}
|
||||
|
||||
// This regex is sourced from the AWS ECR Credential Helper (https://github.com/awslabs/amazon-ecr-credential-helper).
|
||||
|
@ -229,7 +210,6 @@ func (Provider) ParseArtifactRepository(artifactRepository string) (string, erro
|
|||
registry, registryPattern)
|
||||
}
|
||||
|
||||
// For issuing AWS registry credentials the ECR region is required.
|
||||
ecrRegion := parts[0][2]
|
||||
return ecrRegion, nil
|
||||
}
|
||||
|
@ -257,11 +237,8 @@ func (p Provider) NewArtifactRegistryCredentials(ctx context.Context, registryIn
|
|||
|
||||
conf := aws.Config{
|
||||
Region: getECRRegionFromRegistryInput(registryInput),
|
||||
Credentials: accessToken.(*Token).CredentialsProvider(),
|
||||
}
|
||||
|
||||
if hc := o.GetHTTPClient(); hc != nil {
|
||||
conf.HTTPClient = hc
|
||||
Credentials: accessToken.(*Credentials).provider(),
|
||||
HTTPClient: o.GetHTTPClient(),
|
||||
}
|
||||
|
||||
respAny, err := authTokenFunc(ctx, conf)
|
||||
|
@ -315,6 +292,118 @@ func (p Provider) NewArtifactRegistryCredentials(ctx context.Context, registryIn
|
|||
}, nil
|
||||
}
|
||||
|
||||
// GetAccessTokenOptionsForCluster implements auth.Provider.
|
||||
func (Provider) GetAccessTokenOptionsForCluster(opts ...auth.Option) ([][]auth.Option, error) {
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
// ClusterResource is always needed for AWS as we need to extract the region.
|
||||
region, _, err := parseCluster(o.ClusterResource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return [][]auth.Option{{auth.WithSTSRegion(region)}}, nil
|
||||
}
|
||||
|
||||
// NewRESTConfig implements auth.Provider.
|
||||
//
|
||||
// Reference:
|
||||
// https://docs.aws.amazon.com/eks/latest/best-practices/identity-and-access-management.html#_controlling_access_to_eks_clusters
|
||||
func (p Provider) NewRESTConfig(ctx context.Context, accessTokens []auth.Token,
|
||||
opts ...auth.Option) (*auth.RESTConfig, error) {
|
||||
|
||||
// The expiration for an EKS restconfig is always 15 minutes, see the reference above.
|
||||
// Let's record time.Now() on the beginning of the procedure to be on the safe side.
|
||||
expiresAt := time.Now().Add(15 * time.Minute)
|
||||
|
||||
creds := accessTokens[0].(*Credentials).provider()
|
||||
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
hc := o.GetHTTPClient()
|
||||
|
||||
// ClusterResource is always needed for AWS as we need to extract the region.
|
||||
cluster := o.ClusterResource
|
||||
region, clusterName, err := parseCluster(cluster)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Describe the cluster resource to get missing CA or endpoint.
|
||||
host := o.ClusterAddress
|
||||
caData := []byte(o.CAData)
|
||||
if host == "" || len(caData) == 0 {
|
||||
describeInput := &eks.DescribeClusterInput{
|
||||
Name: aws.String(clusterName),
|
||||
}
|
||||
eksOpts := eks.Options{
|
||||
Region: region,
|
||||
Credentials: creds,
|
||||
HTTPClient: hc,
|
||||
}
|
||||
clusterResource, err := p.impl().DescribeCluster(ctx, describeInput, eksOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to describe EKS cluster '%s': %w", cluster, err)
|
||||
}
|
||||
|
||||
// Compare specified address and address from the cluster resource.
|
||||
endpoint := *clusterResource.Cluster.Endpoint
|
||||
if host != "" {
|
||||
canonicalAddress, err := auth.ParseClusterAddress(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse specified cluster address '%s': %w", host, err)
|
||||
}
|
||||
canonicalEndpoint, err := auth.ParseClusterAddress(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse EKS endpoint '%s': %w", endpoint, err)
|
||||
}
|
||||
if canonicalAddress != canonicalEndpoint {
|
||||
return nil, fmt.Errorf("EKS endpoint '%s' does not match specified address: '%s'", endpoint, host)
|
||||
}
|
||||
}
|
||||
|
||||
// Update host and CA with cluster details.
|
||||
host = endpoint
|
||||
if len(caData) == 0 {
|
||||
caData, err = base64.StdEncoding.DecodeString(*clusterResource.Cluster.CertificateAuthority.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode EKS CA certificate: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build token. See reference above.
|
||||
presignOpts := func(po *sts.PresignOptions) {
|
||||
po.Presigner = &eksHTTPPresignerV4{
|
||||
HTTPPresignerV4: po.Presigner,
|
||||
clusterName: clusterName,
|
||||
}
|
||||
}
|
||||
stsOpts := sts.Options{
|
||||
Region: region,
|
||||
Credentials: creds,
|
||||
HTTPClient: hc,
|
||||
}
|
||||
if e := o.STSEndpoint; e != "" {
|
||||
if err := ValidateSTSEndpoint(e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stsOpts.BaseEndpoint = &e
|
||||
}
|
||||
presignedReq, err := p.impl().PresignGetCallerIdentity(ctx, presignOpts, stsOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to presign GetCallerIdentity request: %w", err)
|
||||
}
|
||||
token := fmt.Sprintf("k8s-aws-v1.%s", base64.RawURLEncoding.EncodeToString([]byte(presignedReq.URL)))
|
||||
|
||||
// Build and return the REST config.
|
||||
return &auth.RESTConfig{
|
||||
Host: host,
|
||||
BearerToken: token,
|
||||
CAData: caData,
|
||||
ExpiresAt: expiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p Provider) impl() Implementation {
|
||||
if p.Implementation == nil {
|
||||
return implementation{}
|
||||
|
|
|
@ -68,12 +68,6 @@ func TestProvider_NewControllerToken(t *testing.T) {
|
|||
"please delete/replace the controller pod so the EKS admission controllers can inject this " +
|
||||
"environment variable, or set it manually if the cluster is not EKS",
|
||||
},
|
||||
{
|
||||
name: "missing region but can extract from artifact repository",
|
||||
stsEndpoint: "https://sts.amazonaws.com",
|
||||
artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1",
|
||||
skipSTSRegion: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
@ -85,7 +79,6 @@ func TestProvider_NewControllerToken(t *testing.T) {
|
|||
opts := []auth.Option{
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}),
|
||||
auth.WithSTSEndpoint(tt.stsEndpoint),
|
||||
auth.WithArtifactRepository(tt.artifactRepository),
|
||||
}
|
||||
|
||||
provider := aws.Provider{Implementation: impl}
|
||||
|
@ -93,7 +86,7 @@ func TestProvider_NewControllerToken(t *testing.T) {
|
|||
|
||||
if tt.err == "" {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(token).To(Equal(&aws.Token{Credentials: types.Credentials{
|
||||
g.Expect(token).To(Equal(&aws.Credentials{Credentials: types.Credentials{
|
||||
AccessKeyId: awssdk.String("access-key-id"),
|
||||
SecretAccessKey: awssdk.String(""),
|
||||
SessionToken: awssdk.String(""),
|
||||
|
@ -109,20 +102,9 @@ func TestProvider_NewControllerToken(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
||||
impl := &mockImplementation{
|
||||
t: t,
|
||||
argRegion: "us-east-1",
|
||||
argRoleARN: "arn:aws:iam::1234567890:role/some-role",
|
||||
argRoleSessionName: "test-sa.test-ns.us-east-1.fluxcd.io",
|
||||
argOIDCToken: "oidc-token",
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
argSTSEndpoint: "https://sts.amazonaws.com",
|
||||
returnCreds: awssdk.Credentials{AccessKeyID: "access-key-id"},
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
roleARN string
|
||||
stsEndpoint string
|
||||
artifactRepository string
|
||||
skipSTSRegion bool
|
||||
|
@ -130,53 +112,61 @@ func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "valid",
|
||||
annotations: map[string]string{"eks.amazonaws.com/role-arn": "arn:aws:iam::1234567890:role/some-role"},
|
||||
roleARN: "arn:aws:iam::1234567890:role/some-role",
|
||||
stsEndpoint: "https://sts.amazonaws.com",
|
||||
},
|
||||
{
|
||||
name: "us gov is valid",
|
||||
roleARN: "arn:aws-us-gov:iam::1234567890:role/some-role",
|
||||
stsEndpoint: "https://sts.amazonaws.com",
|
||||
},
|
||||
{
|
||||
name: "invalid sts endpoint",
|
||||
annotations: map[string]string{"eks.amazonaws.com/role-arn": "arn:aws:iam::1234567890:role/some-role"},
|
||||
roleARN: "arn:aws:iam::1234567890:role/some-role",
|
||||
stsEndpoint: "https://something.amazonaws.com",
|
||||
err: `invalid STS endpoint: 'https://something.amazonaws.com'. must match ^https://(.+\.)?sts(-fips)?(\.[^.]+)?(\.vpce)?\.amazonaws\.com$`,
|
||||
},
|
||||
{
|
||||
name: "missing region",
|
||||
annotations: map[string]string{"eks.amazonaws.com/role-arn": "arn:aws:iam::1234567890:role/some-role"},
|
||||
roleARN: "arn:aws:iam::1234567890:role/some-role",
|
||||
stsEndpoint: "https://sts.amazonaws.com",
|
||||
skipSTSRegion: true,
|
||||
err: "an AWS region is required for authenticating with a service account. " +
|
||||
"please configure one in the object spec",
|
||||
},
|
||||
{
|
||||
name: "missing region but can extract from artifact repository",
|
||||
annotations: map[string]string{"eks.amazonaws.com/role-arn": "arn:aws:iam::1234567890:role/some-role"},
|
||||
stsEndpoint: "https://sts.amazonaws.com",
|
||||
artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1",
|
||||
skipSTSRegion: true,
|
||||
},
|
||||
{
|
||||
name: "invalid role ARN",
|
||||
annotations: map[string]string{"eks.amazonaws.com/role-arn": "foobar"},
|
||||
roleARN: "foobar",
|
||||
stsEndpoint: "https://sts.amazonaws.com",
|
||||
err: "invalid eks.amazonaws.com/role-arn annotation: 'foobar'. must match ^arn:aws:iam::[0-9]{1,30}:role/.{1,200}$",
|
||||
err: `invalid eks.amazonaws.com/role-arn annotation: 'foobar'. must match ^arn:aws[\w-]*:iam::[0-9]{1,30}:role/.{1,200}$`,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
impl := &mockImplementation{
|
||||
t: t,
|
||||
argRegion: "us-east-1",
|
||||
argRoleARN: tt.roleARN,
|
||||
argRoleSessionName: "test-sa.test-ns.us-east-1.fluxcd.io",
|
||||
argOIDCToken: "oidc-token",
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
argSTSEndpoint: "https://sts.amazonaws.com",
|
||||
returnCreds: awssdk.Credentials{AccessKeyID: "access-key-id"},
|
||||
}
|
||||
|
||||
oidcToken := "oidc-token"
|
||||
serviceAccount := corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-sa",
|
||||
Namespace: "test-ns",
|
||||
Annotations: tt.annotations,
|
||||
Annotations: map[string]string{"eks.amazonaws.com/role-arn": tt.roleARN},
|
||||
},
|
||||
}
|
||||
|
||||
opts := []auth.Option{
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}),
|
||||
auth.WithSTSEndpoint(tt.stsEndpoint),
|
||||
auth.WithArtifactRepository(tt.artifactRepository),
|
||||
}
|
||||
|
||||
if !tt.skipSTSRegion {
|
||||
|
@ -188,7 +178,7 @@ func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
|||
|
||||
if tt.err == "" {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(token).To(Equal(&aws.Token{Credentials: types.Credentials{
|
||||
g.Expect(token).To(Equal(&aws.Credentials{Credentials: types.Credentials{
|
||||
AccessKeyId: awssdk.String("access-key-id"),
|
||||
SecretAccessKey: awssdk.String(""),
|
||||
SessionToken: awssdk.String(""),
|
||||
|
@ -203,11 +193,11 @@ func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetAudience(t *testing.T) {
|
||||
func TestProvider_GetAudiences(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
aud, err := aws.Provider{}.GetAudience(context.Background(), corev1.ServiceAccount{})
|
||||
aud, err := aws.Provider{}.GetAudiences(context.Background(), corev1.ServiceAccount{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(aud).To(Equal("sts.amazonaws.com"))
|
||||
g.Expect(aud).To(Equal([]string{"sts.amazonaws.com"}))
|
||||
}
|
||||
|
||||
func TestProvider_GetIdentity(t *testing.T) {
|
||||
|
@ -226,28 +216,28 @@ func TestProvider_GetIdentity(t *testing.T) {
|
|||
|
||||
func TestProvider_NewArtifactRegistryCredentials(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
registryInput string
|
||||
expectedPublicECR bool
|
||||
expectedRegion string
|
||||
name string
|
||||
artifactRepository string
|
||||
expectedPublicECR bool
|
||||
expectedRegion string
|
||||
}{
|
||||
{
|
||||
name: "non public ECR",
|
||||
registryInput: "us-east-1",
|
||||
expectedRegion: "us-east-1",
|
||||
expectedPublicECR: false,
|
||||
name: "non public ECR, us-east-1",
|
||||
artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com/foo",
|
||||
expectedRegion: "us-east-1",
|
||||
expectedPublicECR: false,
|
||||
},
|
||||
{
|
||||
name: "non public ECR",
|
||||
registryInput: "us-west-2",
|
||||
expectedRegion: "us-west-2",
|
||||
expectedPublicECR: false,
|
||||
name: "non public ECR, us-west-2",
|
||||
artifactRepository: "012345678901.dkr.ecr.us-west-2.amazonaws.com/foo",
|
||||
expectedRegion: "us-west-2",
|
||||
expectedPublicECR: false,
|
||||
},
|
||||
{
|
||||
name: "public ECR",
|
||||
registryInput: "public.ecr.aws",
|
||||
expectedRegion: "us-east-1", // Public ECR is always us-east-1
|
||||
expectedPublicECR: true,
|
||||
name: "public ECR",
|
||||
artifactRepository: "public.ecr.aws",
|
||||
expectedRegion: "us-east-1", // Public ECR is always us-east-1
|
||||
expectedPublicECR: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -259,24 +249,22 @@ func TestProvider_NewArtifactRegistryCredentials(t *testing.T) {
|
|||
argRegion: tt.expectedRegion,
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
argCredsProvider: credentials.NewStaticCredentialsProvider("access-key-id", "secret-access-key", "session-token"),
|
||||
returnUsername: "username",
|
||||
returnPassword: "password",
|
||||
returnCreds: awssdk.Credentials{
|
||||
AccessKeyID: "access-key-id",
|
||||
SecretAccessKey: "secret-access-key",
|
||||
SessionToken: "session-token",
|
||||
},
|
||||
returnUsername: "username",
|
||||
returnPassword: "password",
|
||||
}
|
||||
|
||||
accessToken := &aws.Token{
|
||||
Credentials: types.Credentials{
|
||||
AccessKeyId: awssdk.String("access-key-id"),
|
||||
SecretAccessKey: awssdk.String("secret-access-key"),
|
||||
SessionToken: awssdk.String("session-token"),
|
||||
},
|
||||
}
|
||||
opts := []auth.Option{
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}),
|
||||
}
|
||||
|
||||
provider := aws.Provider{Implementation: impl}
|
||||
creds, err := provider.NewArtifactRegistryCredentials(
|
||||
context.Background(), tt.registryInput, accessToken, opts...)
|
||||
creds, err := auth.GetArtifactRegistryCredentials(
|
||||
context.Background(), provider, tt.artifactRepository, opts...)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(creds).To(Equal(&auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{
|
||||
|
@ -288,6 +276,19 @@ func TestProvider_NewArtifactRegistryCredentials(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetAccessTokenOptionsForArtifactRepository(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
opts, err := aws.Provider{}.GetAccessTokenOptionsForArtifactRepository(
|
||||
"012345678901.dkr.ecr.us-east-1.amazonaws.com/foo:v1")
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
g.Expect(o.STSRegion).To(Equal("us-east-1"))
|
||||
}
|
||||
|
||||
func TestProvider_ParseArtifactRepository(t *testing.T) {
|
||||
tests := []struct {
|
||||
artifactRepository string
|
||||
|
@ -365,3 +366,130 @@ func TestProvider_ParseArtifactRepository(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_NewRESTConfig(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
cluster string
|
||||
clusterAddress string
|
||||
caData string
|
||||
stsEndpoint string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "valid EKS cluster",
|
||||
cluster: "arn:aws:eks:us-east-1:123456789012:cluster/test-cluster",
|
||||
},
|
||||
{
|
||||
name: "us gov EKS cluster is valid",
|
||||
cluster: "arn:aws-us-gov:eks:us-east-1:123456789012:cluster/test-cluster",
|
||||
},
|
||||
{
|
||||
name: "valid EKS cluster with address match",
|
||||
cluster: "arn:aws:eks:us-east-1:123456789012:cluster/test-cluster",
|
||||
clusterAddress: "https://EXAMPLE1234567890123456789012345678.gr7.us-east-1.eks.amazonaws.com:443",
|
||||
},
|
||||
{
|
||||
name: "valid EKS cluster with CA",
|
||||
cluster: "arn:aws:eks:us-east-1:123456789012:cluster/test-cluster",
|
||||
caData: "-----BEGIN CERTIFICATE-----",
|
||||
},
|
||||
{
|
||||
name: "CA and address only. EKS requires cluster to extract region",
|
||||
clusterAddress: "https://EXAMPLE1234567890123456789012345678.gr7.us-east-1.eks.amazonaws.com:443",
|
||||
caData: "-----BEGIN CERTIFICATE-----",
|
||||
err: `invalid EKS cluster ARN: ''. must match ^arn:aws[\w-]*:eks:([^:]{1,100}):[0-9]{1,30}:cluster/(.{1,200})$`,
|
||||
},
|
||||
{
|
||||
name: "cluster address mismatch",
|
||||
cluster: "arn:aws:eks:us-east-1:123456789012:cluster/test-cluster",
|
||||
clusterAddress: "https://different-endpoint.eks.amazonaws.com:443",
|
||||
err: "EKS endpoint 'https://EXAMPLE1234567890123456789012345678.gr7.us-east-1.eks.amazonaws.com' does not match specified address: 'https://different-endpoint.eks.amazonaws.com:443'",
|
||||
},
|
||||
{
|
||||
name: "valid EKS cluster with custom STS endpoint",
|
||||
cluster: "arn:aws:eks:us-east-1:123456789012:cluster/test-cluster",
|
||||
stsEndpoint: "https://sts.amazonaws.com",
|
||||
},
|
||||
{
|
||||
name: "invalid STS endpoint",
|
||||
cluster: "arn:aws:eks:us-east-1:123456789012:cluster/test-cluster",
|
||||
stsEndpoint: "https://invalid.amazonaws.com",
|
||||
err: `invalid STS endpoint: 'https://invalid.amazonaws.com'. must match ^https://(.+\.)?sts(-fips)?(\.[^.]+)?(\.vpce)?\.amazonaws\.com$`,
|
||||
},
|
||||
{
|
||||
name: "invalid cluster ARN",
|
||||
cluster: "invalid-cluster-arn",
|
||||
err: `invalid EKS cluster ARN: 'invalid-cluster-arn'. must match ^arn:aws[\w-]*:eks:([^:]{1,100}):[0-9]{1,30}:cluster/(.{1,200})$`,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
impl := &mockImplementation{
|
||||
t: t,
|
||||
expectEKSAPICall: tt.clusterAddress == "" || tt.caData == "",
|
||||
argRegion: "us-east-1",
|
||||
argClusterName: "test-cluster",
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
argSTSEndpoint: tt.stsEndpoint,
|
||||
argCredsProvider: credentials.NewStaticCredentialsProvider("access-key-id", "secret-access-key", "session-token"),
|
||||
returnCreds: awssdk.Credentials{AccessKeyID: "access-key-id", SecretAccessKey: "secret-access-key", SessionToken: "session-token"},
|
||||
returnEndpoint: "https://EXAMPLE1234567890123456789012345678.gr7.us-east-1.eks.amazonaws.com",
|
||||
returnCAData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", // base64 encoded "-----BEGIN CERTIFICATE-----"
|
||||
returnPresignedURL: "https://sts.us-east-1.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Algorithm=AWS4-HMAC-SHA256",
|
||||
}
|
||||
|
||||
opts := []auth.Option{
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}),
|
||||
}
|
||||
|
||||
if tt.cluster != "" {
|
||||
opts = append(opts, auth.WithClusterResource(tt.cluster))
|
||||
}
|
||||
|
||||
if tt.clusterAddress != "" {
|
||||
opts = append(opts, auth.WithClusterAddress(tt.clusterAddress))
|
||||
}
|
||||
|
||||
if tt.caData != "" {
|
||||
opts = append(opts, auth.WithCAData(tt.caData))
|
||||
}
|
||||
|
||||
if tt.stsEndpoint != "" {
|
||||
opts = append(opts, auth.WithSTSEndpoint(tt.stsEndpoint))
|
||||
}
|
||||
|
||||
provider := aws.Provider{Implementation: impl}
|
||||
restConfig, err := auth.GetRESTConfig(context.Background(), provider, opts...)
|
||||
|
||||
if tt.err == "" {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(restConfig).NotTo(BeNil())
|
||||
g.Expect(restConfig.Host).To(Equal("https://EXAMPLE1234567890123456789012345678.gr7.us-east-1.eks.amazonaws.com"))
|
||||
g.Expect(restConfig.BearerToken).To(Equal("k8s-aws-v1.aHR0cHM6Ly9zdHMudXMtZWFzdC0xLmFtYXpvbmF3cy5jb20vP0FjdGlvbj1HZXRDYWxsZXJJZGVudGl0eSZWZXJzaW9uPTIwMTEtMDYtMTUmWC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTY"))
|
||||
g.Expect(restConfig.CAData).To(Equal([]byte("-----BEGIN CERTIFICATE-----")))
|
||||
g.Expect(restConfig.ExpiresAt).To(BeTemporally(">", time.Now().Add(14*time.Minute)))
|
||||
g.Expect(restConfig.ExpiresAt).To(BeTemporally("<", time.Now().Add(16*time.Minute)))
|
||||
} else {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.err))
|
||||
g.Expect(restConfig).To(BeNil())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetAccessTokenOptionsForCluster(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
opts, err := aws.Provider{}.GetAccessTokenOptionsForCluster(
|
||||
auth.WithClusterResource("arn:aws:eks:us-west-2:123456789012:cluster/my-cluster"))
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(opts).To(HaveLen(1))
|
||||
|
||||
var o auth.Options
|
||||
o.Apply(opts[0]...)
|
||||
|
||||
g.Expect(o.STSRegion).To(Equal("us-west-2"))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
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 aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
signerv4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
// eksHTTPPresignerV4 implements sts.HTTPPresignerV4 adding the cluster name
|
||||
// to the request header x-k8s-aws-id, as required by EKS authentication.
|
||||
type eksHTTPPresignerV4 struct {
|
||||
sts.HTTPPresignerV4
|
||||
clusterName string
|
||||
}
|
||||
|
||||
// PresignHTTP implements sts.HTTPPresignerV4.
|
||||
func (e *eksHTTPPresignerV4) PresignHTTP(
|
||||
ctx context.Context, credentials aws.Credentials, r *http.Request,
|
||||
payloadHash string, service string, region string, signingTime time.Time,
|
||||
optFns ...func(*signerv4.SignerOptions),
|
||||
) (string, http.Header, error) {
|
||||
r.Header.Add("x-k8s-aws-id", e.clusterName)
|
||||
r.Header.Add("X-Amz-Expires", "900") // ref: https://github.com/aws/aws-sdk-go-v2/issues/1922#issuecomment-1429063756
|
||||
return e.HTTPPresignerV4.PresignHTTP(ctx, credentials, r, payloadHash, service, region, signingTime, optFns...)
|
||||
}
|
|
@ -24,11 +24,11 @@ import (
|
|||
"github.com/aws/aws-sdk-go-v2/service/sts/types"
|
||||
)
|
||||
|
||||
// Token is the AWS token.
|
||||
type Token struct{ types.Credentials }
|
||||
// Credentials is the AWS token.
|
||||
type Credentials struct{ types.Credentials }
|
||||
|
||||
func newTokenFromAWSCredentials(creds *aws.Credentials) *Token {
|
||||
return &Token{types.Credentials{
|
||||
func newTokenFromAWSCredentials(creds *aws.Credentials) *Credentials {
|
||||
return &Credentials{types.Credentials{
|
||||
AccessKeyId: &creds.AccessKeyID,
|
||||
SecretAccessKey: &creds.SecretAccessKey,
|
||||
SessionToken: &creds.SessionToken,
|
||||
|
@ -37,11 +37,10 @@ func newTokenFromAWSCredentials(creds *aws.Credentials) *Token {
|
|||
}
|
||||
|
||||
// GetDuration implements auth.Token.
|
||||
func (t *Token) GetDuration() time.Duration {
|
||||
return time.Until(*t.Expiration)
|
||||
func (c *Credentials) GetDuration() time.Duration {
|
||||
return time.Until(*c.Expiration)
|
||||
}
|
||||
|
||||
// CredentialsProvider gets a credentials provider for the token to use with AWS libraries.
|
||||
func (t *Token) CredentialsProvider() aws.CredentialsProvider {
|
||||
return credentials.NewStaticCredentialsProvider(*t.AccessKeyId, *t.SecretAccessKey, *t.SessionToken)
|
||||
func (c *Credentials) provider() aws.CredentialsProvider {
|
||||
return credentials.NewStaticCredentialsProvider(*c.AccessKeyId, *c.SecretAccessKey, *c.SessionToken)
|
||||
}
|
||||
|
|
|
@ -18,10 +18,12 @@ package azure
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice"
|
||||
)
|
||||
|
||||
// Implementation provides the required methods of the Azure libraries.
|
||||
|
@ -29,7 +31,14 @@ type Implementation interface {
|
|||
NewDefaultAzureCredential(options *azidentity.DefaultAzureCredentialOptions) (azcore.TokenCredential, error)
|
||||
NewDefaultAzureCredentialWithoutShellOut(options *azidentity.DefaultAzureCredentialOptions) (azcore.TokenCredential, error)
|
||||
NewClientAssertionCredential(tenantID string, clientID string, getAssertion func(context.Context) (string, error), options *azidentity.ClientAssertionCredentialOptions) (azcore.TokenCredential, error)
|
||||
SendRequest(req *http.Request, client *http.Client) (*http.Response, error)
|
||||
ExchangeAADAccessTokenForACRRefreshToken(ctx context.Context, client *azcontainerregistry.AuthenticationClient, grantType azcontainerregistry.PostContentSchemaGrantType, service string, options *azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions) (azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse, error)
|
||||
NewManagedClustersClient(subscriptionID string, credential azcore.TokenCredential, options *arm.ClientOptions) (AKSClient, error)
|
||||
}
|
||||
|
||||
// AKSClient provides the required methods of the AKS client.
|
||||
type AKSClient interface {
|
||||
Get(ctx context.Context, resourceGroupName string, resourceName string, options *armcontainerservice.ManagedClustersClientGetOptions) (armcontainerservice.ManagedClustersClientGetResponse, error)
|
||||
ListClusterUserCredentials(ctx context.Context, resourceGroupName string, resourceName string, options *armcontainerservice.ManagedClustersClientListClusterUserCredentialsOptions) (armcontainerservice.ManagedClustersClientListClusterUserCredentialsResponse, error)
|
||||
}
|
||||
|
||||
type implementation struct{}
|
||||
|
@ -46,6 +55,10 @@ func (implementation) NewClientAssertionCredential(tenantID string, clientID str
|
|||
return azidentity.NewClientAssertionCredential(tenantID, clientID, getAssertion, options)
|
||||
}
|
||||
|
||||
func (implementation) SendRequest(req *http.Request, client *http.Client) (*http.Response, error) {
|
||||
return client.Do(req)
|
||||
func (implementation) ExchangeAADAccessTokenForACRRefreshToken(ctx context.Context, client *azcontainerregistry.AuthenticationClient, grantType azcontainerregistry.PostContentSchemaGrantType, service string, options *azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions) (azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse, error) {
|
||||
return client.ExchangeAADAccessTokenForACRRefreshToken(ctx, grantType, service, options)
|
||||
}
|
||||
|
||||
func (implementation) NewManagedClustersClient(subscriptionID string, credential azcore.TokenCredential, options *arm.ClientOptions) (AKSClient, error) {
|
||||
return armcontainerservice.NewManagedClustersClient(subscriptionID, credential, options)
|
||||
}
|
||||
|
|
|
@ -18,15 +18,22 @@ package azure_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/fluxcd/pkg/auth/azure"
|
||||
)
|
||||
|
||||
type mockImplementation struct {
|
||||
|
@ -34,16 +41,28 @@ type mockImplementation struct {
|
|||
|
||||
shellOut bool
|
||||
|
||||
argTenantID string
|
||||
argClientID string
|
||||
argOIDCToken string
|
||||
argURL string
|
||||
argBody string
|
||||
argProxyURL *url.URL
|
||||
argScopes []string
|
||||
expectAKSAPICall bool
|
||||
|
||||
returnResp *http.Response
|
||||
returnToken string
|
||||
argTenantID string
|
||||
argClientID string
|
||||
argOIDCToken string
|
||||
argProxyURL *url.URL
|
||||
argScopes []string
|
||||
argToken string
|
||||
argRegistry string
|
||||
argSubscription string
|
||||
argResourceGroup string
|
||||
argClusterName string
|
||||
|
||||
// For dual-token flow (RESTConfig)
|
||||
argFirstScopes []string
|
||||
argSecondScopes []string
|
||||
firstCallMade bool
|
||||
|
||||
returnToken string
|
||||
returnACRToken string
|
||||
returnCluster armcontainerservice.ManagedCluster
|
||||
returnKubeconfigs []*armcontainerservice.CredentialResult
|
||||
}
|
||||
|
||||
type mockTokenCredential struct {
|
||||
|
@ -54,6 +73,16 @@ type mockTokenCredential struct {
|
|||
returnToken string
|
||||
}
|
||||
|
||||
type mockAKSClient struct {
|
||||
t *testing.T
|
||||
|
||||
argResourceGroup string
|
||||
argClusterName string
|
||||
|
||||
returnCluster armcontainerservice.ManagedCluster
|
||||
returnKubeconfigs []*armcontainerservice.CredentialResult
|
||||
}
|
||||
|
||||
func (m *mockImplementation) NewDefaultAzureCredential(options *azidentity.DefaultAzureCredentialOptions) (azcore.TokenCredential, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
|
@ -79,7 +108,19 @@ func (m *mockImplementation) newDefaultAzureCredential(options *azidentity.Defau
|
|||
proxyURL, err := options.Transport.(*http.Client).Transport.(*http.Transport).Proxy(nil)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(proxyURL).To(Equal(m.argProxyURL))
|
||||
return &mockTokenCredential{t: m.t, argScopes: m.argScopes, returnToken: m.returnToken}, nil
|
||||
|
||||
// Determine which scopes to expect based on dual-token flow
|
||||
expectedScopes := m.argScopes
|
||||
if m.argFirstScopes != nil && m.argSecondScopes != nil {
|
||||
if !m.firstCallMade {
|
||||
expectedScopes = m.argFirstScopes
|
||||
m.firstCallMade = true
|
||||
} else {
|
||||
expectedScopes = m.argSecondScopes
|
||||
}
|
||||
}
|
||||
|
||||
return &mockTokenCredential{t: m.t, argScopes: expectedScopes, returnToken: m.returnToken}, nil
|
||||
}
|
||||
|
||||
func (m *mockImplementation) NewClientAssertionCredential(tenantID string, clientID string, getAssertion func(context.Context) (string, error), options *azidentity.ClientAssertionCredentialOptions) (azcore.TokenCredential, error) {
|
||||
|
@ -100,33 +141,128 @@ func (m *mockImplementation) NewClientAssertionCredential(tenantID string, clien
|
|||
proxyURL, err := options.Transport.(*http.Client).Transport.(*http.Transport).Proxy(nil)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(proxyURL).To(Equal(m.argProxyURL))
|
||||
return &mockTokenCredential{t: m.t, argScopes: m.argScopes, returnToken: m.returnToken}, nil
|
||||
|
||||
// Determine which scopes to expect based on dual-token flow
|
||||
expectedScopes := m.argScopes
|
||||
if m.argFirstScopes != nil && m.argSecondScopes != nil {
|
||||
if !m.firstCallMade {
|
||||
expectedScopes = m.argFirstScopes
|
||||
m.firstCallMade = true
|
||||
} else {
|
||||
expectedScopes = m.argSecondScopes
|
||||
}
|
||||
}
|
||||
|
||||
return &mockTokenCredential{t: m.t, argScopes: expectedScopes, returnToken: m.returnToken}, nil
|
||||
}
|
||||
|
||||
func (m *mockImplementation) SendRequest(req *http.Request, client *http.Client) (*http.Response, error) {
|
||||
func (m *mockImplementation) ExchangeAADAccessTokenForACRRefreshToken(ctx context.Context, client *azcontainerregistry.AuthenticationClient, grantType azcontainerregistry.PostContentSchemaGrantType, service string, options *azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions) (azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(req).NotTo(BeNil())
|
||||
g.Expect(req.Method).To(Equal(http.MethodPost))
|
||||
g.Expect(req.URL).NotTo(BeNil())
|
||||
g.Expect(req.URL.String()).To(Equal(m.argURL))
|
||||
g.Expect(req.Body).NotTo(BeNil())
|
||||
b, err := io.ReadAll(req.Body)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(string(b)).To(Equal(m.argBody))
|
||||
g.Expect(client).NotTo(BeNil())
|
||||
g.Expect(client.Transport).NotTo(BeNil())
|
||||
g.Expect(client.Transport.(*http.Transport)).NotTo(BeNil())
|
||||
g.Expect(client.Transport.(*http.Transport).Proxy).NotTo(BeNil())
|
||||
proxyURL, err := client.Transport.(*http.Transport).Proxy(nil)
|
||||
|
||||
// Assert registry endpoint.
|
||||
endpointField := reflect.ValueOf(client).Elem().FieldByName("endpoint")
|
||||
endpointValue := reflect.NewAt(endpointField.Type(), unsafe.Pointer(endpointField.UnsafeAddr())).Elem().Interface().(string)
|
||||
g.Expect(endpointValue).To(Equal("https://" + m.argRegistry))
|
||||
|
||||
// Assert proxy URL.
|
||||
azcoreClientField := reflect.ValueOf(client).Elem().FieldByName("internal")
|
||||
azcoreClientValue := reflect.NewAt(azcoreClientField.Type(), unsafe.Pointer(azcoreClientField.UnsafeAddr())).Elem().Interface().(*azcore.Client)
|
||||
g.Expect(azcoreClientValue).NotTo(BeNil())
|
||||
pipeline := azcoreClientValue.Pipeline()
|
||||
g.Expect(pipeline).NotTo(BeNil())
|
||||
pipelineValue := reflect.ValueOf(pipeline)
|
||||
pipelinePtr := reflect.New(pipelineValue.Type())
|
||||
pipelinePtr.Elem().Set(pipelineValue)
|
||||
policiesField := pipelinePtr.Elem().FieldByName("policies")
|
||||
policiesValue := reflect.NewAt(policiesField.Type(), unsafe.Pointer(policiesField.UnsafeAddr())).Elem().Interface().([]policy.Policy)
|
||||
g.Expect(policiesValue).NotTo(BeNil())
|
||||
transportPolicy := policiesValue[len(policiesValue)-1]
|
||||
transportPolicyValue := reflect.ValueOf(transportPolicy)
|
||||
transportPolicyPtr := reflect.New(transportPolicyValue.Type())
|
||||
transportPolicyPtr.Elem().Set(transportPolicyValue)
|
||||
transportField := transportPolicyPtr.Elem().FieldByName("trans")
|
||||
transportValue := reflect.NewAt(transportField.Type(), unsafe.Pointer(transportField.UnsafeAddr())).Elem().Interface().(policy.Transporter)
|
||||
g.Expect(transportValue).NotTo(BeNil())
|
||||
g.Expect(transportValue.(*http.Client)).NotTo(BeNil())
|
||||
g.Expect(transportValue.(*http.Client).Transport).NotTo(BeNil())
|
||||
g.Expect(transportValue.(*http.Client).Transport.(*http.Transport)).NotTo(BeNil())
|
||||
g.Expect(transportValue.(*http.Client).Transport.(*http.Transport).Proxy).NotTo(BeNil())
|
||||
proxyURL, err := transportValue.(*http.Client).Transport.(*http.Transport).Proxy(nil)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(proxyURL).To(Equal(m.argProxyURL))
|
||||
return m.returnResp, nil
|
||||
|
||||
// Assert trivial inputs.
|
||||
g.Expect(grantType).To(Equal(azcontainerregistry.PostContentSchemaGrantTypeAccessToken))
|
||||
g.Expect(service).To(Equal(m.argRegistry))
|
||||
g.Expect(options).To(Equal(&azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions{
|
||||
AccessToken: &m.argToken,
|
||||
}))
|
||||
|
||||
return azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenResponse{
|
||||
ACRRefreshToken: azcontainerregistry.ACRRefreshToken{RefreshToken: &m.returnACRToken},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockImplementation) NewManagedClustersClient(subscriptionID string, credential azcore.TokenCredential, options *arm.ClientOptions) (azure.AKSClient, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(m.expectAKSAPICall).To(BeTrue())
|
||||
g.Expect(subscriptionID).To(Equal(m.argSubscription))
|
||||
g.Expect(credential).NotTo(BeNil())
|
||||
token, err := credential.GetToken(context.Background(), policy.TokenRequestOptions{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(token.Token).To(Equal(m.argToken))
|
||||
g.Expect(options).NotTo(BeNil())
|
||||
g.Expect(options.Transport).NotTo(BeNil())
|
||||
g.Expect(options.Transport.(*http.Client)).NotTo(BeNil())
|
||||
g.Expect(options.Transport.(*http.Client).Transport).NotTo(BeNil())
|
||||
g.Expect(options.Transport.(*http.Client).Transport.(*http.Transport)).NotTo(BeNil())
|
||||
g.Expect(options.Transport.(*http.Client).Transport.(*http.Transport).Proxy).NotTo(BeNil())
|
||||
proxyURL, err := options.Transport.(*http.Client).Transport.(*http.Transport).Proxy(nil)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(proxyURL).To(Equal(m.argProxyURL))
|
||||
return &mockAKSClient{
|
||||
t: m.t,
|
||||
argResourceGroup: m.argResourceGroup,
|
||||
argClusterName: m.argClusterName,
|
||||
returnCluster: m.returnCluster,
|
||||
returnKubeconfigs: m.returnKubeconfigs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockAKSClient) Get(ctx context.Context, resourceGroupName string, resourceName string, options *armcontainerservice.ManagedClustersClientGetOptions) (armcontainerservice.ManagedClustersClientGetResponse, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(ctx).NotTo(BeNil())
|
||||
g.Expect(resourceGroupName).To(Equal(m.argResourceGroup))
|
||||
g.Expect(resourceName).To(Equal(m.argClusterName))
|
||||
g.Expect(options).To(BeNil())
|
||||
return armcontainerservice.ManagedClustersClientGetResponse{
|
||||
ManagedCluster: m.returnCluster,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockAKSClient) ListClusterUserCredentials(ctx context.Context, resourceGroupName string, resourceName string, options *armcontainerservice.ManagedClustersClientListClusterUserCredentialsOptions) (armcontainerservice.ManagedClustersClientListClusterUserCredentialsResponse, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(ctx).NotTo(BeNil())
|
||||
g.Expect(resourceGroupName).To(Equal(m.argResourceGroup))
|
||||
g.Expect(resourceName).To(Equal(m.argClusterName))
|
||||
g.Expect(options).To(BeNil())
|
||||
return armcontainerservice.ManagedClustersClientListClusterUserCredentialsResponse{
|
||||
CredentialResults: armcontainerservice.CredentialResults{
|
||||
Kubeconfigs: m.returnKubeconfigs,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockTokenCredential) GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(options.Scopes).To(Equal(m.argScopes))
|
||||
return azcore.AccessToken{Token: m.returnToken}, nil
|
||||
return azcore.AccessToken{
|
||||
Token: m.returnToken,
|
||||
ExpiresOn: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), // Fixed expiry for testing
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -17,13 +17,13 @@ limitations under the License.
|
|||
package azure
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
)
|
||||
|
||||
func getIdentity(serviceAccount corev1.ServiceAccount) (string, error) {
|
||||
|
@ -54,34 +54,95 @@ func getClientID(serviceAccount corev1.ServiceAccount) (string, error) {
|
|||
return "", fmt.Errorf("azure client ID is not set in the service account annotation %s", key)
|
||||
}
|
||||
|
||||
func getScopes(o *auth.Options) []string {
|
||||
if acrScope := getACRScope(o.ArtifactRepository); acrScope != "" {
|
||||
return []string{acrScope}
|
||||
const clusterPattern = `(?i)^/subscriptions/([^/]{36})/resourceGroups/([^/]{1,200})/providers/Microsoft\.ContainerService/managedClusters/([^/]{1,200})$`
|
||||
|
||||
var clusterRegex = regexp.MustCompile(clusterPattern)
|
||||
|
||||
func parseCluster(cluster string) (string, string, string, error) {
|
||||
m := clusterRegex.FindStringSubmatch(cluster)
|
||||
if len(m) != 4 {
|
||||
return "", "", "", fmt.Errorf("invalid AKS cluster ID: '%s'. must match %s",
|
||||
cluster, clusterPattern)
|
||||
}
|
||||
return o.Scopes
|
||||
subscriptionID := m[1]
|
||||
resourceGroup := m[2]
|
||||
clusterName := m[3]
|
||||
return subscriptionID, resourceGroup, clusterName, nil
|
||||
}
|
||||
|
||||
func getACRScope(artifactRepository string) string {
|
||||
if artifactRepository == "" {
|
||||
return ""
|
||||
}
|
||||
// envVarAzureEnvironmentFilepath is the environment variable name used to specify the path of the configuration file with custom Azure endpoints.
|
||||
const envVarAzureEnvironmentFilepath = "AZURE_ENVIRONMENT_FILEPATH"
|
||||
|
||||
registry, err := auth.GetRegistryFromArtifactRepository(artifactRepository)
|
||||
// Environment is used to read the Azure environment configuration from a JSON file, it is a subset of the struct defined in
|
||||
// https://github.com/kubernetes-sigs/cloud-provider-azure/blob/e68bd888a7616d52f45f39238691f32821884120/pkg/azclient/cloud.go#L152-L185
|
||||
// with exact same field names and json annotations.
|
||||
// We define this struct here for two reasons:
|
||||
// 1. We are not aware of any libraries we could import this struct from.
|
||||
// 2. We don't use all the fields defined in the original struct.
|
||||
type Environment struct {
|
||||
ContainerRegistryDNSSuffix string `json:"containerRegistryDNSSuffix,omitempty"`
|
||||
ResourceManagerEndpoint string `json:"resourceManagerEndpoint,omitempty"`
|
||||
TokenAudience string `json:"tokenAudience,omitempty"`
|
||||
}
|
||||
|
||||
// hasEnvironmentFile checks if the environment variable AZURE_ENVIRONMENT_FILEPATH is set
|
||||
func hasEnvironmentFile() bool {
|
||||
_, ok := os.LookupEnv(envVarAzureEnvironmentFilepath)
|
||||
return ok
|
||||
}
|
||||
|
||||
// getEnvironmentConfig reads the Azure environment configuration from a JSON file
|
||||
// located at the path specified by the environment variable AZURE_ENVIRONMENT_FILEPATH.
|
||||
// Call hasEnvironmentFile() before calling this function to ensure the file exists.
|
||||
func getEnvironmentConfig() (*Environment, error) {
|
||||
envFilePath := os.Getenv(envVarAzureEnvironmentFilepath)
|
||||
if len(envFilePath) == 0 {
|
||||
return nil, fmt.Errorf("environment variable %s is not set", envVarAzureEnvironmentFilepath)
|
||||
}
|
||||
content, err := os.ReadFile(envFilePath)
|
||||
if err != nil {
|
||||
// it's ok to swallow the error here, it should never happen
|
||||
// because GetRegistryFromArtifactRepository() is already called
|
||||
// earlier by auth.GetToken() and the error is handled there.
|
||||
return ""
|
||||
return nil, err
|
||||
}
|
||||
env := &Environment{}
|
||||
if err = json.Unmarshal(content, env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var conf *cloud.Configuration
|
||||
switch {
|
||||
case strings.HasSuffix(registry, ".azurecr.cn"):
|
||||
conf = &cloud.AzureChina
|
||||
case strings.HasSuffix(registry, ".azurecr.us"):
|
||||
conf = &cloud.AzureGovernment
|
||||
default:
|
||||
conf = &cloud.AzurePublic
|
||||
}
|
||||
return conf.Services[cloud.ResourceManager].Endpoint + "/" + ".default"
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// getCloudConfigFromEnvironment reads the Azure environment configuration and returns a cloud.Configuration object.
|
||||
func getCloudConfigFromEnvironment() (*cloud.Configuration, error) {
|
||||
env, err := getEnvironmentConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cloudConf := cloud.Configuration{
|
||||
Services: make(map[cloud.ServiceName]cloud.ServiceConfiguration),
|
||||
}
|
||||
if len(env.ResourceManagerEndpoint) > 0 && len(env.TokenAudience) > 0 {
|
||||
cloudConf.Services[cloud.ResourceManager] = cloud.ServiceConfiguration{
|
||||
Endpoint: env.ResourceManagerEndpoint,
|
||||
Audience: env.TokenAudience,
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("resourceManagerEndpoint and tokenAudience must be set in the environment file")
|
||||
}
|
||||
|
||||
return &cloudConf, nil
|
||||
}
|
||||
|
||||
// getContainerRegistryDNSSuffix reads the Azure environment configuration and returns the container registry DNS suffix.
|
||||
func getContainerRegistryDNSSuffix() (string, error) {
|
||||
env, err := getEnvironmentConfig()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(env.ContainerRegistryDNSSuffix) == 0 {
|
||||
return "", fmt.Errorf("containerRegistryDNSSuffix must be set in the environment file")
|
||||
}
|
||||
|
||||
return env.ContainerRegistryDNSSuffix, nil
|
||||
}
|
||||
|
|
|
@ -18,19 +18,22 @@ package azure
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
)
|
||||
|
@ -52,10 +55,10 @@ func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (
|
|||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
var azOpts azidentity.DefaultAzureCredentialOptions
|
||||
|
||||
if hc := o.GetHTTPClient(); hc != nil {
|
||||
azOpts.Transport = hc
|
||||
azOpts := azidentity.DefaultAzureCredentialOptions{
|
||||
ClientOptions: azcore.ClientOptions{
|
||||
Transport: o.GetHTTPClient(),
|
||||
},
|
||||
}
|
||||
|
||||
credFunc := p.impl().NewDefaultAzureCredentialWithoutShellOut
|
||||
|
@ -67,7 +70,7 @@ func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (
|
|||
return nil, err
|
||||
}
|
||||
token, err := cred.GetToken(ctx, policy.TokenRequestOptions{
|
||||
Scopes: getScopes(&o),
|
||||
Scopes: o.Scopes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -76,9 +79,9 @@ func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (
|
|||
return &Token{token}, nil
|
||||
}
|
||||
|
||||
// GetAudience implements auth.Provider.
|
||||
func (Provider) GetAudience(context.Context, corev1.ServiceAccount) (string, error) {
|
||||
return "api://AzureADTokenExchange", nil
|
||||
// GetAudiences implements auth.Provider.
|
||||
func (Provider) GetAudiences(context.Context, corev1.ServiceAccount) ([]string, error) {
|
||||
return []string{"api://AzureADTokenExchange"}, nil
|
||||
}
|
||||
|
||||
// GetIdentity implements auth.Provider.
|
||||
|
@ -100,10 +103,10 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
|
|||
s := strings.Split(identity, "/")
|
||||
tenantID, clientID := s[0], s[1]
|
||||
|
||||
azOpts := &azidentity.ClientAssertionCredentialOptions{}
|
||||
|
||||
if hc := o.GetHTTPClient(); hc != nil {
|
||||
azOpts.Transport = hc
|
||||
azOpts := &azidentity.ClientAssertionCredentialOptions{
|
||||
ClientOptions: azcore.ClientOptions{
|
||||
Transport: o.GetHTTPClient(),
|
||||
},
|
||||
}
|
||||
|
||||
cred, err := p.impl().NewClientAssertionCredential(tenantID, clientID, func(context.Context) (string, error) {
|
||||
|
@ -113,7 +116,7 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
|
|||
return nil, err
|
||||
}
|
||||
token, err := cred.GetToken(ctx, policy.TokenRequestOptions{
|
||||
Scopes: getScopes(&o),
|
||||
Scopes: o.Scopes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -122,78 +125,106 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
|
|||
return &Token{token}, nil
|
||||
}
|
||||
|
||||
// GetAccessTokenOptionsForArtifactRepository implements auth.Provider.
|
||||
func (p Provider) GetAccessTokenOptionsForArtifactRepository(artifactRepository string) ([]auth.Option, error) {
|
||||
// Azure requires scopes for getting access tokens. Here we compute
|
||||
// the scope for ACR, which is based on the registry host.
|
||||
|
||||
registry, err := auth.GetRegistryFromArtifactRepository(artifactRepository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var conf *cloud.Configuration
|
||||
switch {
|
||||
case hasEnvironmentFile():
|
||||
var err error
|
||||
conf, err = getCloudConfigFromEnvironment()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case strings.HasSuffix(registry, ".azurecr.cn"):
|
||||
conf = &cloud.AzureChina
|
||||
case strings.HasSuffix(registry, ".azurecr.us"):
|
||||
conf = &cloud.AzureGovernment
|
||||
default:
|
||||
conf = &cloud.AzurePublic
|
||||
}
|
||||
acrScope := conf.Services[cloud.ResourceManager].Endpoint + "/.default"
|
||||
|
||||
return []auth.Option{auth.WithScopes(acrScope)}, nil
|
||||
}
|
||||
|
||||
// https://github.com/kubernetes/kubernetes/blob/v1.23.1/pkg/credentialprovider/azure/azure_credentials.go#L55
|
||||
const registryPattern = `^.+\.(azurecr\.io|azurecr\.cn|azurecr\.de|azurecr\.us)$`
|
||||
|
||||
var registryRegex = regexp.MustCompile(registryPattern)
|
||||
|
||||
// ParseArtifactRepository implements auth.Provider.
|
||||
// ParseArtifactRepository returns the ACR registry URL.
|
||||
// ParseArtifactRepository returns the ACR registry host.
|
||||
func (Provider) ParseArtifactRepository(artifactRepository string) (string, error) {
|
||||
registry, err := auth.GetRegistryFromArtifactRepository(artifactRepository)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !registryRegex.MatchString(registry) {
|
||||
return "", fmt.Errorf("invalid Azure registry: '%s'. must match %s",
|
||||
registry, registryPattern)
|
||||
// For issuing Azure registry credentials the registry host is required.
|
||||
if registryRegex.MatchString(registry) {
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
// For issuing Azure registry credentials the registry URL is required.
|
||||
registryURL := fmt.Sprintf("https://%s", registry)
|
||||
return registryURL, nil
|
||||
// Check if environment variable is configured for container registry suffix
|
||||
if hasEnvironmentFile() {
|
||||
// Load the environment configuration from the file
|
||||
registrySuffix, err := getContainerRegistryDNSSuffix()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get container registry suffix from environment file: %w", err)
|
||||
}
|
||||
if strings.HasSuffix(registry, registrySuffix) {
|
||||
return registry, nil
|
||||
}
|
||||
return "", fmt.Errorf("invalid Azure registry: '%s'. must end with %s",
|
||||
registry, registrySuffix)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("invalid Azure registry: '%s'. must match %s",
|
||||
registry, registryPattern)
|
||||
}
|
||||
|
||||
// NewArtifactRegistryCredentials implements auth.Provider.
|
||||
func (p Provider) NewArtifactRegistryCredentials(ctx context.Context, registryURL string,
|
||||
func (p Provider) NewArtifactRegistryCredentials(ctx context.Context, registry string,
|
||||
accessToken auth.Token, opts ...auth.Option) (*auth.ArtifactRegistryCredentials, error) {
|
||||
|
||||
t := accessToken.(*Token)
|
||||
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
// Build request.
|
||||
exchangeURL, err := url.Parse(registryURL)
|
||||
// Create the ACR authentication client.
|
||||
endpoint := fmt.Sprintf("https://%s", registry)
|
||||
clientOpts := azcontainerregistry.AuthenticationClientOptions{
|
||||
ClientOptions: azcore.ClientOptions{
|
||||
Transport: o.GetHTTPClient(),
|
||||
},
|
||||
}
|
||||
client, err := azcontainerregistry.NewAuthenticationClient(endpoint, &clientOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exchangeURL.Path = path.Join(exchangeURL.Path, "oauth2/exchange")
|
||||
parameters := url.Values{}
|
||||
parameters.Add("grant_type", "access_token")
|
||||
parameters.Add("service", exchangeURL.Hostname())
|
||||
parameters.Add("access_token", t.Token)
|
||||
body := strings.NewReader(parameters.Encode())
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, exchangeURL.String(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
// Send request.
|
||||
httpClient := http.DefaultClient
|
||||
if hc := o.GetHTTPClient(); hc != nil {
|
||||
httpClient = hc
|
||||
// Exchange the access token for an ACR token.
|
||||
grantType := azcontainerregistry.PostContentSchemaGrantTypeAccessToken
|
||||
service := registry
|
||||
tokenOpts := &azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions{
|
||||
AccessToken: &accessToken.(*Token).Token,
|
||||
}
|
||||
resp, err := p.impl().SendRequest(req, httpClient)
|
||||
resp, err := p.impl().ExchangeAADAccessTokenForACRRefreshToken(ctx, client, grantType, service, tokenOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
token := *resp.RefreshToken
|
||||
|
||||
// Parse response.
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status from ACR exchange request: %d", resp.StatusCode)
|
||||
}
|
||||
var tokenResp struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Parse the refresh token to get the expiry time.
|
||||
var claims jwt.MapClaims
|
||||
if _, _, err := jwt.NewParser().ParseUnverified(tokenResp.RefreshToken, &claims); err != nil {
|
||||
if _, _, err := jwt.NewParser().ParseUnverified(token, &claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expiry, err := claims.GetExpirationTime()
|
||||
|
@ -201,16 +232,159 @@ func (p Provider) NewArtifactRegistryCredentials(ctx context.Context, registryUR
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Return the credentials.
|
||||
return &auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{
|
||||
// https://docs.microsoft.com/en-us/azure/container-registry/container-registry-authentication?tabs=azure-cli#az-acr-login-with---expose-token
|
||||
Username: "00000000-0000-0000-0000-000000000000",
|
||||
Password: tokenResp.RefreshToken,
|
||||
Password: token,
|
||||
}),
|
||||
ExpiresAt: expiry.Time,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAccessTokenOptionsForCluster implements auth.Provider.
|
||||
func (Provider) GetAccessTokenOptionsForCluster(opts ...auth.Option) ([][]auth.Option, error) {
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
var atOpts [][]auth.Option
|
||||
|
||||
// Token used for impersonating the Managed Identity inside the AKS cluster.
|
||||
const aksScope = "6dae42f8-4368-4678-94ff-3960e28e3630/.default"
|
||||
aksTokenOpts := []auth.Option{auth.WithScopes(aksScope)}
|
||||
atOpts = append(atOpts, aksTokenOpts)
|
||||
|
||||
// Token needed for looking up details of the cluster resource.
|
||||
if o.ClusterAddress == "" || o.CAData == "" {
|
||||
conf := &cloud.AzurePublic
|
||||
switch authorityHost := os.Getenv("AZURE_AUTHORITY_HOST"); {
|
||||
case hasEnvironmentFile():
|
||||
var err error
|
||||
conf, err = getCloudConfigFromEnvironment()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case strings.Contains(authorityHost, "chinacloudapi.cn"):
|
||||
conf = &cloud.AzureChina
|
||||
case strings.Contains(authorityHost, "microsoftonline.us"):
|
||||
conf = &cloud.AzureGovernment
|
||||
}
|
||||
armScope := conf.Services[cloud.ResourceManager].Audience + "/.default"
|
||||
armTokenOpts := []auth.Option{auth.WithScopes(armScope)}
|
||||
atOpts = append(atOpts, armTokenOpts)
|
||||
}
|
||||
|
||||
return atOpts, nil
|
||||
}
|
||||
|
||||
// NewRESTConfig implements auth.Provider.
|
||||
func (p Provider) NewRESTConfig(ctx context.Context, accessTokens []auth.Token,
|
||||
opts ...auth.Option) (*auth.RESTConfig, error) {
|
||||
|
||||
aksToken := accessTokens[0].(*Token)
|
||||
|
||||
var armToken *Token
|
||||
if len(accessTokens) == 2 {
|
||||
armToken = accessTokens[1].(*Token)
|
||||
}
|
||||
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
// Describe the cluster resource to get missing CA or endpoint.
|
||||
host := o.ClusterAddress
|
||||
caData := []byte(o.CAData)
|
||||
if host == "" || len(caData) == 0 {
|
||||
cluster := o.ClusterResource
|
||||
subscriptionID, resourceGroup, clusterName, err := parseCluster(cluster)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create client for describing the cluster resource.
|
||||
clientOpts := arm.ClientOptions{
|
||||
ClientOptions: azcore.ClientOptions{
|
||||
Transport: o.GetHTTPClient(),
|
||||
},
|
||||
}
|
||||
client, err := p.impl().NewManagedClustersClient(
|
||||
subscriptionID, armToken.credential(), &clientOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create client for describing AKS cluster: %w", err)
|
||||
}
|
||||
|
||||
// Describe the cluster resource.
|
||||
clusterResource, err := client.Get(ctx, resourceGroup, clusterName, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to describe AKS cluster: %w", err)
|
||||
}
|
||||
|
||||
// We only support clusters with Microsoft Entra ID integration enabled.
|
||||
if clusterResource.Properties.AADProfile == nil {
|
||||
return nil, fmt.Errorf("AKS cluster %s does not have Microsoft Entra ID integration enabled. "+
|
||||
"See docs for enabling: https://learn.microsoft.com/en-us/azure/aks/enable-authentication-microsoft-entra-id",
|
||||
cluster)
|
||||
}
|
||||
|
||||
// Parse specified cluster address.
|
||||
var canonicalHost string
|
||||
if host != "" {
|
||||
var err error
|
||||
canonicalHost, err = auth.ParseClusterAddress(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse specified cluster address '%s': %w", host, err)
|
||||
}
|
||||
}
|
||||
|
||||
// List kubeconfigs for this AKS cluster. We need to find the one
|
||||
// matching the canonical address, or the first one if no address
|
||||
// is specified.
|
||||
resp, err := client.ListClusterUserCredentials(ctx, resourceGroup, clusterName, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var restConfig *rest.Config
|
||||
var addresses []string
|
||||
for i, kc := range resp.Kubeconfigs {
|
||||
conf, err := clientcmd.RESTConfigFromKubeConfig(kc.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse kubeconfig[%d]: %w", i, err)
|
||||
}
|
||||
addresses = append(addresses, fmt.Sprintf("'%s'", conf.Host))
|
||||
canonicalHostFromAPI, err := auth.ParseClusterAddress(conf.Host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse address '%s' from kubeconfig[%d]: %w", conf.Host, i, err)
|
||||
}
|
||||
if canonicalHost == "" || canonicalHostFromAPI == canonicalHost {
|
||||
restConfig = conf
|
||||
break
|
||||
}
|
||||
}
|
||||
if restConfig == nil {
|
||||
if canonicalHost == "" {
|
||||
return nil, fmt.Errorf("no kubeconfig found for AKS cluster %s", cluster)
|
||||
}
|
||||
return nil, fmt.Errorf("AKS cluster %s does not match specified address '%s'. cluster addresses: [%s]",
|
||||
cluster, o.ClusterAddress, strings.Join(addresses, ", "))
|
||||
}
|
||||
|
||||
// Update host and CA with cluster details.
|
||||
host = restConfig.Host
|
||||
if len(caData) == 0 {
|
||||
caData = restConfig.CAData
|
||||
}
|
||||
}
|
||||
|
||||
// Build and return the REST config.
|
||||
return &auth.RESTConfig{
|
||||
Host: host,
|
||||
BearerToken: aksToken.Token,
|
||||
CAData: caData,
|
||||
ExpiresAt: aksToken.ExpiresOn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p Provider) impl() Implementation {
|
||||
if p.Implementation == nil {
|
||||
return implementation{}
|
||||
|
|
|
@ -21,14 +21,13 @@ import (
|
|||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
. "github.com/onsi/gomega"
|
||||
|
@ -40,60 +39,51 @@ import (
|
|||
)
|
||||
|
||||
func TestProvider_NewControllerToken(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
shellOut bool
|
||||
}{
|
||||
{
|
||||
name: "without shell out",
|
||||
shellOut: false,
|
||||
},
|
||||
{
|
||||
name: "with shell out",
|
||||
shellOut: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
impl := &mockImplementation{
|
||||
t: t,
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
argScopes: []string{"scope1", "scope2"},
|
||||
returnToken: "access-token",
|
||||
impl := &mockImplementation{
|
||||
t: t,
|
||||
shellOut: tt.shellOut,
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
argScopes: []string{"scope1", "scope2"},
|
||||
returnToken: "access-token",
|
||||
}
|
||||
|
||||
opts := []auth.Option{
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
}
|
||||
|
||||
if tt.shellOut {
|
||||
opts = append(opts, auth.WithAllowShellOut())
|
||||
}
|
||||
|
||||
provider := azure.Provider{Implementation: impl}
|
||||
token, err := provider.NewControllerToken(context.Background(), opts...)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(token).To(Equal(&azure.Token{AccessToken: azcore.AccessToken{
|
||||
Token: "access-token",
|
||||
ExpiresOn: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}}))
|
||||
})
|
||||
}
|
||||
|
||||
opts := []auth.Option{
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
}
|
||||
|
||||
provider := azure.Provider{Implementation: impl}
|
||||
token, err := provider.NewControllerToken(context.Background(), opts...)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(token).To(Equal(&azure.Token{AccessToken: azcore.AccessToken{Token: "access-token"}}))
|
||||
}
|
||||
|
||||
func TestProvider_NewControllerTokenWithShellOut(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
impl := &mockImplementation{
|
||||
t: t,
|
||||
shellOut: true,
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
argScopes: []string{"scope1", "scope2"},
|
||||
returnToken: "access-token",
|
||||
}
|
||||
|
||||
opts := []auth.Option{
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithAllowShellOut(),
|
||||
}
|
||||
|
||||
provider := azure.Provider{Implementation: impl}
|
||||
token, err := provider.NewControllerToken(context.Background(), opts...)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(token).To(Equal(&azure.Token{AccessToken: azcore.AccessToken{Token: "access-token"}}))
|
||||
}
|
||||
|
||||
func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
||||
impl := &mockImplementation{
|
||||
t: t,
|
||||
argTenantID: "tenant-id",
|
||||
argClientID: "client-id",
|
||||
argOIDCToken: "oidc-token",
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
argScopes: []string{"scope1", "scope2"},
|
||||
returnToken: "access-token",
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
|
@ -124,6 +114,16 @@ func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
impl := &mockImplementation{
|
||||
t: t,
|
||||
argTenantID: "tenant-id",
|
||||
argClientID: "client-id",
|
||||
argOIDCToken: "oidc-token",
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
argScopes: []string{"scope1", "scope2"},
|
||||
returnToken: "access-token",
|
||||
}
|
||||
|
||||
oidcToken := "oidc-token"
|
||||
serviceAccount := corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
@ -140,7 +140,10 @@ func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
|||
|
||||
if tt.err == "" {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(token).To(Equal(&azure.Token{AccessToken: azcore.AccessToken{Token: "access-token"}}))
|
||||
g.Expect(token).To(Equal(&azure.Token{AccessToken: azcore.AccessToken{
|
||||
Token: "access-token",
|
||||
ExpiresOn: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}}))
|
||||
} else {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(Equal(tt.err))
|
||||
|
@ -150,11 +153,11 @@ func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetAudience(t *testing.T) {
|
||||
func TestProvider_GetAudiences(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
aud, err := azure.Provider{}.GetAudience(context.Background(), corev1.ServiceAccount{})
|
||||
aud, err := azure.Provider{}.GetAudiences(context.Background(), corev1.ServiceAccount{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(aud).To(Equal("api://AzureADTokenExchange"))
|
||||
g.Expect(aud).To(Equal([]string{"api://AzureADTokenExchange"}))
|
||||
}
|
||||
|
||||
func TestProvider_GetIdentity(t *testing.T) {
|
||||
|
@ -204,36 +207,24 @@ func TestProvider_NewArtifactRegistryCredentials(t *testing.T) {
|
|||
g := NewWithT(t)
|
||||
|
||||
impl := &mockImplementation{
|
||||
t: t,
|
||||
argURL: fmt.Sprintf("https://%s/oauth2/exchange", tt.registry),
|
||||
argBody: fmt.Sprintf("access_token=access-token&grant_type=access_token&service=%s", tt.registry),
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
argScopes: []string{tt.expectedScope},
|
||||
returnToken: "access-token",
|
||||
returnResp: &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(fmt.Sprintf(`{"refresh_token":"%s"}`, refreshToken))),
|
||||
},
|
||||
t: t,
|
||||
argRegistry: tt.registry,
|
||||
argToken: "access-token",
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
argScopes: []string{tt.expectedScope},
|
||||
returnToken: "access-token",
|
||||
returnACRToken: refreshToken,
|
||||
}
|
||||
provider := azure.Provider{Implementation: impl}
|
||||
|
||||
artifactRepository := fmt.Sprintf("%s/repo", tt.registry)
|
||||
opts := []auth.Option{
|
||||
auth.WithArtifactRepository(artifactRepository),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}),
|
||||
}
|
||||
|
||||
registryURL, err := provider.ParseArtifactRepository(artifactRepository)
|
||||
creds, err := auth.GetArtifactRegistryCredentials(context.Background(), provider, artifactRepository, opts...)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(registryURL).To(Equal(fmt.Sprintf("https://%s", tt.registry)))
|
||||
|
||||
accessToken, err := provider.NewControllerToken(context.Background(), opts...)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(accessToken).To(Equal(&azure.Token{AccessToken: azcore.AccessToken{Token: "access-token"}}))
|
||||
|
||||
token, err := provider.NewArtifactRegistryCredentials(context.Background(), registryURL, accessToken, opts...)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(token).To(Equal(&auth.ArtifactRegistryCredentials{
|
||||
g.Expect(creds).To(Equal(&auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{
|
||||
Username: "00000000-0000-0000-0000-000000000000",
|
||||
Password: refreshToken,
|
||||
|
@ -246,46 +237,69 @@ func TestProvider_NewArtifactRegistryCredentials(t *testing.T) {
|
|||
|
||||
func TestProvider_ParseArtifactRegistry(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
artifactRepository string
|
||||
expectedRegistryURL string
|
||||
expectValid bool
|
||||
artifactRepository string
|
||||
expectedRegistryURL string
|
||||
containerRegistryDNSSuffix string
|
||||
expectValid bool
|
||||
}{
|
||||
{
|
||||
artifactRepository: "foo.azurecr.io",
|
||||
expectedRegistryURL: "https://foo.azurecr.io",
|
||||
artifactRepository: "foo.azurecr.io/repo",
|
||||
expectedRegistryURL: "foo.azurecr.io",
|
||||
expectValid: true,
|
||||
},
|
||||
{
|
||||
artifactRepository: "foo.azurecr.cn",
|
||||
expectedRegistryURL: "https://foo.azurecr.cn",
|
||||
artifactRepository: "foo.azurecr.cn/repo",
|
||||
expectedRegistryURL: "foo.azurecr.cn",
|
||||
expectValid: true,
|
||||
},
|
||||
{
|
||||
artifactRepository: "foo.azurecr.de",
|
||||
expectedRegistryURL: "https://foo.azurecr.de",
|
||||
artifactRepository: "foo.azurecr.de/repo",
|
||||
expectedRegistryURL: "foo.azurecr.de",
|
||||
expectValid: true,
|
||||
},
|
||||
{
|
||||
artifactRepository: "foo.azurecr.us",
|
||||
expectedRegistryURL: "https://foo.azurecr.us",
|
||||
artifactRepository: "foo.azurecr.us/repo",
|
||||
expectedRegistryURL: "foo.azurecr.us",
|
||||
expectValid: true,
|
||||
},
|
||||
{
|
||||
artifactRepository: "foo.azurecr.com",
|
||||
artifactRepository: "foo.azurecr.com/repo",
|
||||
expectValid: false,
|
||||
},
|
||||
{
|
||||
artifactRepository: ".azurecr.io",
|
||||
artifactRepository: ".azurecr.io/repo",
|
||||
expectValid: false,
|
||||
},
|
||||
{
|
||||
artifactRepository: "012345678901.dkr.ecr.us-east-1.amazonaws.com",
|
||||
expectValid: false,
|
||||
},
|
||||
{
|
||||
artifactRepository: "foo.azurecr.private/repo",
|
||||
expectedRegistryURL: "foo.azurecr.private",
|
||||
containerRegistryDNSSuffix: "azurecr.private",
|
||||
expectValid: true,
|
||||
},
|
||||
{
|
||||
artifactRepository: "foo.azurecr.private/repo",
|
||||
expectedRegistryURL: "foo.azurecr.private",
|
||||
containerRegistryDNSSuffix: "azurecr.pr",
|
||||
expectValid: false,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.artifactRepository, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Create a temporary JSON file if containerRegistryDNS is defined
|
||||
if tt.containerRegistryDNSSuffix != "" {
|
||||
envContent := fmt.Sprintf(`{"containerRegistryDNSSuffix": "%s"}`, tt.containerRegistryDNSSuffix)
|
||||
tempFileName, err := createTempAzureEnvFile(envContent)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
defer os.Remove(tempFileName)
|
||||
|
||||
// Set the environment variable to point to the temp file
|
||||
t.Setenv("AZURE_ENVIRONMENT_FILEPATH", tempFileName)
|
||||
}
|
||||
registryURL, err := azure.Provider{}.ParseArtifactRepository(tt.artifactRepository)
|
||||
|
||||
if tt.expectValid {
|
||||
|
@ -298,3 +312,399 @@ func TestProvider_ParseArtifactRegistry(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetAccessTokenOptionsForArtifactRepository(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
artifactRepository string
|
||||
readFromEnv bool
|
||||
expectedScope string
|
||||
}{
|
||||
{
|
||||
name: "Azure Public Cloud",
|
||||
artifactRepository: "myregistry.azurecr.io",
|
||||
expectedScope: "https://management.azure.com/.default",
|
||||
},
|
||||
{
|
||||
name: "Azure China Cloud",
|
||||
artifactRepository: "myregistry.azurecr.cn",
|
||||
expectedScope: "https://management.chinacloudapi.cn/.default",
|
||||
},
|
||||
{
|
||||
name: "Azure Government Cloud",
|
||||
artifactRepository: "myregistry.azurecr.us",
|
||||
expectedScope: "https://management.usgovcloudapi.net/.default",
|
||||
},
|
||||
{
|
||||
name: "Invalid registry",
|
||||
artifactRepository: "myregistry.invalid.io",
|
||||
expectedScope: "https://management.azure.com/.default",
|
||||
},
|
||||
{
|
||||
name: "Custom environment file",
|
||||
artifactRepository: "myregistry.private.io",
|
||||
readFromEnv: true,
|
||||
expectedScope: "https://management.core.azure.private/.default",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
if tt.readFromEnv {
|
||||
envContent := fmt.Sprintf(`{"resourceManagerEndpoint": "%s", "tokenAudience": "%s", "extraField": "%s"}`, "https://management.core.azure.private", "https://management.core.azure.private", "random-extra-field-for-testing")
|
||||
tempFileName, err := createTempAzureEnvFile(envContent)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
defer os.Remove(tempFileName)
|
||||
|
||||
// Set the environment variable to point to the temp file
|
||||
t.Setenv("AZURE_ENVIRONMENT_FILEPATH", tempFileName)
|
||||
}
|
||||
|
||||
provider := azure.Provider{}
|
||||
opts, err := provider.GetAccessTokenOptionsForArtifactRepository(tt.artifactRepository)
|
||||
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(opts).To(HaveLen(1))
|
||||
|
||||
var armOptions auth.Options
|
||||
armOptions.Apply(opts...)
|
||||
g.Expect(armOptions.Scopes).To(Equal([]string{tt.expectedScope}))
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_NewRESTConfig(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
cluster string
|
||||
clusterAddress string
|
||||
caData string
|
||||
aadProfile *armcontainerservice.ManagedClusterAADProfile
|
||||
kubeconfigs []*armcontainerservice.CredentialResult
|
||||
authorityHost string
|
||||
secondScope string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "valid AKS cluster",
|
||||
cluster: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster",
|
||||
aadProfile: &armcontainerservice.ManagedClusterAADProfile{
|
||||
Managed: &[]bool{true}[0],
|
||||
},
|
||||
kubeconfigs: []*armcontainerservice.CredentialResult{
|
||||
{
|
||||
Name: &[]string{"clusterUser"}[0],
|
||||
Value: createKubeconfig("test-cluster", "https://test-cluster-12345678.hcp.eastus.azmk8s.io:443"),
|
||||
},
|
||||
{
|
||||
Name: &[]string{"clusterUser-secondary"}[0],
|
||||
Value: createKubeconfig("test-cluster-secondary", "https://test-cluster-secondary-87654321.hcp.westus.azmk8s.io:443"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid AKS cluster - china",
|
||||
cluster: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster",
|
||||
aadProfile: &armcontainerservice.ManagedClusterAADProfile{
|
||||
Managed: &[]bool{true}[0],
|
||||
},
|
||||
kubeconfigs: []*armcontainerservice.CredentialResult{
|
||||
{
|
||||
Name: &[]string{"clusterUser"}[0],
|
||||
Value: createKubeconfig("test-cluster", "https://test-cluster-12345678.hcp.eastus.azmk8s.io:443"),
|
||||
},
|
||||
{
|
||||
Name: &[]string{"clusterUser-secondary"}[0],
|
||||
Value: createKubeconfig("test-cluster-secondary", "https://test-cluster-secondary-87654321.hcp.westus.azmk8s.io:443"),
|
||||
},
|
||||
},
|
||||
authorityHost: "https://login.chinacloudapi.cn/",
|
||||
secondScope: "https://management.core.chinacloudapi.cn/.default",
|
||||
},
|
||||
{
|
||||
name: "valid AKS cluster - us gov",
|
||||
cluster: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster",
|
||||
aadProfile: &armcontainerservice.ManagedClusterAADProfile{
|
||||
Managed: &[]bool{true}[0],
|
||||
},
|
||||
kubeconfigs: []*armcontainerservice.CredentialResult{
|
||||
{
|
||||
Name: &[]string{"clusterUser"}[0],
|
||||
Value: createKubeconfig("test-cluster", "https://test-cluster-12345678.hcp.eastus.azmk8s.io:443"),
|
||||
},
|
||||
{
|
||||
Name: &[]string{"clusterUser-secondary"}[0],
|
||||
Value: createKubeconfig("test-cluster-secondary", "https://test-cluster-secondary-87654321.hcp.westus.azmk8s.io:443"),
|
||||
},
|
||||
},
|
||||
authorityHost: "https://login.microsoftonline.us/",
|
||||
secondScope: "https://management.core.usgovcloudapi.net/.default",
|
||||
},
|
||||
{
|
||||
name: "valid AKS cluster - lowercase",
|
||||
cluster: "/subscriptions/12345678-1234-1234-1234-123456789012/resourcegroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster",
|
||||
aadProfile: &armcontainerservice.ManagedClusterAADProfile{
|
||||
Managed: &[]bool{true}[0],
|
||||
},
|
||||
kubeconfigs: []*armcontainerservice.CredentialResult{
|
||||
{
|
||||
Name: &[]string{"clusterUser"}[0],
|
||||
Value: createKubeconfig("test-cluster", "https://test-cluster-12345678.hcp.eastus.azmk8s.io:443"),
|
||||
},
|
||||
{
|
||||
Name: &[]string{"clusterUser-secondary"}[0],
|
||||
Value: createKubeconfig("test-cluster-secondary", "https://test-cluster-secondary-87654321.hcp.westus.azmk8s.io:443"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid AKS cluster with address match",
|
||||
cluster: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster",
|
||||
clusterAddress: "https://test-cluster-secondary-87654321.hcp.westus.azmk8s.io:443",
|
||||
aadProfile: &armcontainerservice.ManagedClusterAADProfile{
|
||||
Managed: &[]bool{true}[0],
|
||||
},
|
||||
kubeconfigs: []*armcontainerservice.CredentialResult{
|
||||
{
|
||||
Name: &[]string{"clusterUser"}[0],
|
||||
Value: createKubeconfig("test-cluster", "https://test-cluster-12345678.hcp.eastus.azmk8s.io:443"),
|
||||
},
|
||||
{
|
||||
Name: &[]string{"clusterUser-secondary"}[0],
|
||||
Value: createKubeconfig("test-cluster-secondary", "https://test-cluster-secondary-87654321.hcp.westus.azmk8s.io:443"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid AKS cluster with CA",
|
||||
cluster: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster",
|
||||
caData: "-----BEGIN CERTIFICATE-----",
|
||||
aadProfile: &armcontainerservice.ManagedClusterAADProfile{
|
||||
Managed: &[]bool{true}[0],
|
||||
},
|
||||
kubeconfigs: []*armcontainerservice.CredentialResult{
|
||||
{
|
||||
Name: &[]string{"clusterUser"}[0],
|
||||
Value: createKubeconfig("test-cluster", "https://test-cluster-12345678.hcp.eastus.azmk8s.io:443"),
|
||||
},
|
||||
{
|
||||
Name: &[]string{"clusterUser-secondary"}[0],
|
||||
Value: createKubeconfig("test-cluster-secondary", "https://test-cluster-secondary-87654321.hcp.westus.azmk8s.io:443"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CA and address only",
|
||||
clusterAddress: "https://test-cluster-secondary-87654321.hcp.westus.azmk8s.io:443",
|
||||
caData: "-----BEGIN CERTIFICATE-----",
|
||||
aadProfile: &armcontainerservice.ManagedClusterAADProfile{
|
||||
Managed: &[]bool{true}[0],
|
||||
},
|
||||
kubeconfigs: []*armcontainerservice.CredentialResult{
|
||||
{
|
||||
Name: &[]string{"clusterUser"}[0],
|
||||
Value: createKubeconfig("test-cluster", "https://test-cluster-12345678.hcp.eastus.azmk8s.io:443"),
|
||||
},
|
||||
{
|
||||
Name: &[]string{"clusterUser-secondary"}[0],
|
||||
Value: createKubeconfig("test-cluster-secondary", "https://test-cluster-secondary-87654321.hcp.westus.azmk8s.io:443"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cluster address mismatch",
|
||||
cluster: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster",
|
||||
clusterAddress: "https://different-cluster.hcp.eastus.azmk8s.io:443",
|
||||
aadProfile: &armcontainerservice.ManagedClusterAADProfile{
|
||||
Managed: &[]bool{true}[0],
|
||||
},
|
||||
kubeconfigs: []*armcontainerservice.CredentialResult{
|
||||
{
|
||||
Name: &[]string{"clusterUser"}[0],
|
||||
Value: createKubeconfig("test-cluster", "https://test-cluster-12345678.hcp.eastus.azmk8s.io:443"),
|
||||
},
|
||||
},
|
||||
err: "AKS cluster /subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster does not match specified address 'https://different-cluster.hcp.eastus.azmk8s.io:443'. cluster addresses: ['https://test-cluster-12345678.hcp.eastus.azmk8s.io:443']",
|
||||
},
|
||||
{
|
||||
name: "cluster without AAD integration",
|
||||
cluster: "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster",
|
||||
err: "AKS cluster /subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster does not have Microsoft Entra ID integration enabled. See docs for enabling: https://learn.microsoft.com/en-us/azure/aks/enable-authentication-microsoft-entra-id",
|
||||
},
|
||||
{
|
||||
name: "invalid cluster ID",
|
||||
cluster: "invalid-cluster-id",
|
||||
err: `invalid AKS cluster ID: 'invalid-cluster-id'. must match (?i)^/subscriptions/([^/]{36})/resourceGroups/([^/]{1,200})/providers/Microsoft\.ContainerService/managedClusters/([^/]{1,200})$`,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
if tt.authorityHost != "" {
|
||||
t.Setenv("AZURE_AUTHORITY_HOST", tt.authorityHost)
|
||||
}
|
||||
|
||||
secondScope := "https://management.core.windows.net//.default"
|
||||
if tt.secondScope != "" {
|
||||
secondScope = tt.secondScope
|
||||
}
|
||||
|
||||
impl := &mockImplementation{
|
||||
t: t,
|
||||
expectAKSAPICall: tt.clusterAddress == "" || tt.caData == "",
|
||||
argToken: "access-token",
|
||||
argFirstScopes: []string{"6dae42f8-4368-4678-94ff-3960e28e3630/.default"},
|
||||
argSecondScopes: []string{secondScope},
|
||||
argSubscription: "12345678-1234-1234-1234-123456789012",
|
||||
argResourceGroup: "test-rg",
|
||||
argClusterName: "test-cluster",
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
returnToken: "access-token",
|
||||
returnCluster: armcontainerservice.ManagedCluster{
|
||||
Properties: &armcontainerservice.ManagedClusterProperties{
|
||||
AADProfile: tt.aadProfile,
|
||||
},
|
||||
},
|
||||
returnKubeconfigs: tt.kubeconfigs,
|
||||
}
|
||||
|
||||
opts := []auth.Option{
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}),
|
||||
}
|
||||
|
||||
if tt.cluster != "" {
|
||||
opts = append(opts, auth.WithClusterResource(tt.cluster))
|
||||
}
|
||||
|
||||
if tt.clusterAddress != "" {
|
||||
opts = append(opts, auth.WithClusterAddress(tt.clusterAddress))
|
||||
}
|
||||
|
||||
if tt.caData != "" {
|
||||
opts = append(opts, auth.WithCAData(tt.caData))
|
||||
}
|
||||
|
||||
provider := azure.Provider{Implementation: impl}
|
||||
restConfig, err := auth.GetRESTConfig(context.Background(), provider, opts...)
|
||||
|
||||
if tt.err == "" {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(restConfig).NotTo(BeNil())
|
||||
expectedHost := "https://test-cluster-12345678.hcp.eastus.azmk8s.io:443"
|
||||
if tt.clusterAddress != "" {
|
||||
expectedHost = tt.clusterAddress
|
||||
}
|
||||
g.Expect(restConfig.Host).To(Equal(expectedHost))
|
||||
g.Expect(restConfig.BearerToken).To(Equal("access-token"))
|
||||
g.Expect(restConfig.CAData).To(Equal([]byte("-----BEGIN CERTIFICATE-----")))
|
||||
g.Expect(restConfig.ExpiresAt).To(Equal(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)))
|
||||
} else {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.err))
|
||||
g.Expect(restConfig).To(BeNil())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetAccessTokenOptionsForCluster(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
t.Run("needs to fetch cluster", func(t *testing.T) {
|
||||
opts, err := azure.Provider{}.GetAccessTokenOptionsForCluster(
|
||||
auth.WithClusterResource("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster"))
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(opts).To(HaveLen(2))
|
||||
|
||||
// AKS token options
|
||||
var aksOptions auth.Options
|
||||
aksOptions.Apply(opts[0]...)
|
||||
g.Expect(aksOptions.Scopes).To(Equal([]string{"6dae42f8-4368-4678-94ff-3960e28e3630/.default"}))
|
||||
|
||||
// ARM token options
|
||||
var armOptions auth.Options
|
||||
armOptions.Apply(opts[1]...)
|
||||
g.Expect(armOptions.Scopes).To(Equal([]string{"https://management.core.windows.net//.default"}))
|
||||
})
|
||||
|
||||
t.Run("needs to fetch cluster arm options from env", func(t *testing.T) {
|
||||
envContent := fmt.Sprintf(`{"resourceManagerEndpoint": "%s", "tokenAudience": "%s", "extraField": "%s"}`, "https://management.core.azure.private/", "https://management.core.azure.private/", "random-extra-field-for-testing")
|
||||
tempFileName, err := createTempAzureEnvFile(envContent)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
defer os.Remove(tempFileName)
|
||||
|
||||
// Set the environment variable to point to the temp file
|
||||
t.Setenv("AZURE_ENVIRONMENT_FILEPATH", tempFileName)
|
||||
|
||||
opts, err := azure.Provider{}.GetAccessTokenOptionsForCluster(
|
||||
auth.WithClusterResource("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.ContainerService/managedClusters/test-cluster"))
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(opts).To(HaveLen(2))
|
||||
|
||||
// AKS token options
|
||||
var aksOptions auth.Options
|
||||
aksOptions.Apply(opts[0]...)
|
||||
g.Expect(aksOptions.Scopes).To(Equal([]string{"6dae42f8-4368-4678-94ff-3960e28e3630/.default"}))
|
||||
|
||||
// ARM token options
|
||||
var armOptions auth.Options
|
||||
armOptions.Apply(opts[1]...)
|
||||
g.Expect(armOptions.Scopes).To(Equal([]string{"https://management.core.azure.private//.default"}))
|
||||
})
|
||||
|
||||
t.Run("no need to fetch cluster", func(t *testing.T) {
|
||||
opts, err := azure.Provider{}.GetAccessTokenOptionsForCluster(
|
||||
auth.WithClusterAddress("https://test-cluster-12345678.hcp.eastus.azmk8s.io:443"),
|
||||
auth.WithCAData("-----BEGIN CERTIFICATE-----"))
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(opts).To(HaveLen(1))
|
||||
|
||||
// AKS token options
|
||||
var aksOptions auth.Options
|
||||
aksOptions.Apply(opts[0]...)
|
||||
g.Expect(aksOptions.Scopes).To(Equal([]string{"6dae42f8-4368-4678-94ff-3960e28e3630/.default"}))
|
||||
})
|
||||
}
|
||||
|
||||
func createKubeconfig(clusterName, serverURL string) []byte {
|
||||
return []byte(fmt.Sprintf(`apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t
|
||||
server: %s
|
||||
name: %s
|
||||
contexts:
|
||||
- context:
|
||||
cluster: %s
|
||||
user: clusterUser_test-rg_%s
|
||||
name: %s
|
||||
current-context: %s
|
||||
kind: Config
|
||||
users:
|
||||
- name: clusterUser_test-rg_%s
|
||||
user:
|
||||
exec:
|
||||
apiVersion: client.authentication.k8s.io/v1beta1
|
||||
command: kubelogin
|
||||
env: null
|
||||
`, serverURL, clusterName, clusterName, clusterName, clusterName, clusterName, clusterName))
|
||||
}
|
||||
|
||||
func createTempAzureEnvFile(content string) (string, error) {
|
||||
tempFile, err := os.CreateTemp("", "azure_env_*.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := tempFile.Close(); err != nil {
|
||||
os.Remove(tempFile.Name())
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(tempFile.Name(), []byte(content), 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tempFile.Name(), nil
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
package azure
|
||||
|
||||
const (
|
||||
// https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops#q-can-i-use-a-service-principal-or-managed-identity-with-azure-cli
|
||||
// https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops#q-can-i-add-a-managed-identity-from-a-different-tenant-to-my-organization
|
||||
ScopeDevOps = "499b84ac-1321-427f-aa17-267ca6975798/.default"
|
||||
|
||||
// https://github.com/Azure/azure-sdk-for-go/blob/f5dfe3b53fe63aacd3aeba948bbe21d961edf376/sdk/storage/azqueue/internal/shared/shared.go#L18
|
||||
|
|
|
@ -34,8 +34,7 @@ func (t *Token) GetDuration() time.Duration {
|
|||
return time.Until(t.ExpiresOn)
|
||||
}
|
||||
|
||||
// Credential gets a token credential for the token to use with Azure libraries.
|
||||
func (t *Token) Credential() azcore.TokenCredential {
|
||||
func (t *Token) credential() azcore.TokenCredential {
|
||||
return &staticTokenCredential{t.AccessToken}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,15 +38,16 @@ func NewTokenCredential(ctx context.Context, opts ...auth.Option) azcore.TokenCr
|
|||
|
||||
// GetToken implements exported.TokenCredential.
|
||||
// The context is ignored, use the constructor to set the context.
|
||||
// This is because some callers of the library pass context.Background()
|
||||
// when calling this method (e.g. SOPS), so to ensure we have a real
|
||||
// context we pass it in the constructor.
|
||||
// This is because the GCP abstraction does not receive a context
|
||||
// in the method arguments, so we unfortunately need to standardize
|
||||
// the behavior of all providers around this so the usage of this
|
||||
// library can be consistent regardless of the provider.
|
||||
func (t *tokenCredential) GetToken(_ context.Context, tokenOpts policy.TokenRequestOptions) (azcore.AccessToken, error) {
|
||||
opts := t.opts
|
||||
if tokenOpts.Scopes != nil {
|
||||
opts = append(opts, auth.WithScopes(tokenOpts.Scopes...))
|
||||
}
|
||||
token, err := auth.GetToken(t.ctx, Provider{}, opts...)
|
||||
token, err := auth.GetAccessToken(t.ctx, Provider{}, opts...)
|
||||
if err != nil {
|
||||
return azcore.AccessToken{}, err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func buildCacheKey(parts ...string) string {
|
||||
s := strings.Join(parts, "\n")
|
||||
hash := sha256.Sum256([]byte(s))
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
|
@ -22,12 +22,14 @@ import (
|
|||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"golang.org/x/oauth2/google/externalaccount"
|
||||
"google.golang.org/api/container/v1"
|
||||
)
|
||||
|
||||
// Implementation provides the required methods of the GCP libraries.
|
||||
type Implementation interface {
|
||||
DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSource, error)
|
||||
NewTokenSource(ctx context.Context, conf externalaccount.Config) (oauth2.TokenSource, error)
|
||||
GetCluster(ctx context.Context, cluster string, client *container.Service) (*container.Cluster, error)
|
||||
}
|
||||
|
||||
type implementation struct{}
|
||||
|
@ -39,3 +41,7 @@ func (implementation) DefaultTokenSource(ctx context.Context, scope ...string) (
|
|||
func (implementation) NewTokenSource(ctx context.Context, conf externalaccount.Config) (oauth2.TokenSource, error) {
|
||||
return externalaccount.NewTokenSource(ctx, conf)
|
||||
}
|
||||
|
||||
func (implementation) GetCluster(ctx context.Context, cluster string, client *container.Service) (*container.Cluster, error) {
|
||||
return client.Projects.Locations.Clusters.Get(cluster).Context(ctx).Do()
|
||||
}
|
||||
|
|
|
@ -20,20 +20,28 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google/externalaccount"
|
||||
"google.golang.org/api/container/v1"
|
||||
)
|
||||
|
||||
type mockImplementation struct {
|
||||
t *testing.T
|
||||
|
||||
expectGKEAPICall bool
|
||||
|
||||
argConfig externalaccount.Config
|
||||
argProxyURL *url.URL
|
||||
argCluster string
|
||||
|
||||
returnToken *oauth2.Token
|
||||
returnToken *oauth2.Token
|
||||
returnCluster *container.Cluster
|
||||
}
|
||||
|
||||
func (m *mockImplementation) DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSource, error) {
|
||||
|
@ -70,3 +78,35 @@ func (m *mockImplementation) NewTokenSource(ctx context.Context, conf externalac
|
|||
g.Expect(conf).To(Equal(m.argConfig))
|
||||
return oauth2.StaticTokenSource(m.returnToken), nil
|
||||
}
|
||||
|
||||
func (m *mockImplementation) GetCluster(ctx context.Context, cluster string, client *container.Service) (*container.Cluster, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(m.expectGKEAPICall).To(BeTrue())
|
||||
g.Expect(ctx).NotTo(BeNil())
|
||||
g.Expect(cluster).To(Equal(m.argCluster))
|
||||
g.Expect(client).NotTo(BeNil())
|
||||
g.Expect(client.BasePath).To(Equal("https://container.googleapis.com/"))
|
||||
httpClientField := reflect.ValueOf(client).Elem().FieldByName("client")
|
||||
httpClientValue := reflect.NewAt(httpClientField.Type(), unsafe.Pointer(httpClientField.UnsafeAddr())).Elem().Interface().(*http.Client)
|
||||
g.Expect(httpClientValue).NotTo(BeNil())
|
||||
g.Expect(httpClientValue.Transport).NotTo(BeNil())
|
||||
g.Expect(httpClientValue.Transport.(*oauth2.Transport)).NotTo(BeNil())
|
||||
g.Expect(httpClientValue.Transport.(*oauth2.Transport).Source).NotTo(BeNil())
|
||||
token, err := httpClientValue.Transport.(*oauth2.Transport).Source.Token()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(token).To(Equal(m.returnToken))
|
||||
g.Expect(httpClientValue.Transport.(*oauth2.Transport).Base).NotTo(BeNil())
|
||||
g.Expect(httpClientValue.Transport.(*oauth2.Transport).Base.(*otelhttp.Transport)).NotTo(BeNil())
|
||||
otelRoundTripperField := reflect.ValueOf(httpClientValue.Transport.(*oauth2.Transport).Base.(*otelhttp.Transport)).Elem().FieldByName("rt")
|
||||
otelRoundTripperValue := reflect.NewAt(otelRoundTripperField.Type(), unsafe.Pointer(otelRoundTripperField.UnsafeAddr())).Elem().Interface()
|
||||
g.Expect(otelRoundTripperValue).NotTo(BeNil())
|
||||
parameterRoundTripperField := reflect.ValueOf(otelRoundTripperValue).Elem().FieldByName("base")
|
||||
parameterRoundTripperValue := reflect.NewAt(parameterRoundTripperField.Type(), unsafe.Pointer(parameterRoundTripperField.UnsafeAddr())).Elem().Interface()
|
||||
g.Expect(parameterRoundTripperValue.(*http.Transport)).NotTo(BeNil())
|
||||
g.Expect(parameterRoundTripperValue.(*http.Transport).Proxy).NotTo(BeNil())
|
||||
proxyURL, err := parameterRoundTripperValue.(*http.Transport).Proxy(nil)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(proxyURL).To(Equal(m.argProxyURL))
|
||||
return m.returnCluster, nil
|
||||
}
|
||||
|
|
|
@ -56,3 +56,15 @@ func getWorkloadIdentityProviderAudience(serviceAccount corev1.ServiceAccount) (
|
|||
}
|
||||
return fmt.Sprintf("//iam.googleapis.com/%s", wip), nil
|
||||
}
|
||||
|
||||
const clusterPattern = `^projects/[^/]{1,200}/locations/[^/]{1,200}/clusters/[^/]{1,200}$`
|
||||
|
||||
var clusterRegex = regexp.MustCompile(clusterPattern)
|
||||
|
||||
func parseCluster(cluster string) error {
|
||||
if !clusterRegex.MatchString(cluster) {
|
||||
return fmt.Errorf("invalid GKE cluster ID: '%s'. must match %s",
|
||||
cluster, clusterPattern)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -18,12 +18,17 @@ package gcp
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google/externalaccount"
|
||||
"google.golang.org/api/container/v1"
|
||||
"google.golang.org/api/option"
|
||||
htransport "google.golang.org/api/transport/http"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
auth "github.com/fluxcd/pkg/auth"
|
||||
|
@ -50,9 +55,7 @@ func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (
|
|||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
if hc := o.GetHTTPClient(); hc != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, hc)
|
||||
}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, o.GetHTTPClient())
|
||||
|
||||
src, err := p.impl().DefaultTokenSource(ctx, scopes...)
|
||||
if err != nil {
|
||||
|
@ -66,20 +69,25 @@ func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (
|
|||
return &Token{*token}, nil
|
||||
}
|
||||
|
||||
// GetAudience implements auth.Provider.
|
||||
func (Provider) GetAudience(ctx context.Context, serviceAccount corev1.ServiceAccount) (string, error) {
|
||||
// GetAudiences implements auth.Provider.
|
||||
func (Provider) GetAudiences(ctx context.Context, serviceAccount corev1.ServiceAccount) ([]string, error) {
|
||||
|
||||
// Check if a workload identity provider is specified in the service account.
|
||||
// If so, the current cluster is not GKE and the audience is the provider itself.
|
||||
audience, err := getWorkloadIdentityProviderAudience(serviceAccount)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
if audience != "" {
|
||||
return audience, nil
|
||||
return []string{audience}, nil
|
||||
}
|
||||
|
||||
// Assume we are in GKE. In this case, the audience is the workload identity pool.
|
||||
return gkeMetadata.workloadIdentityPool(ctx)
|
||||
audience, err = gkeMetadata.workloadIdentityPool(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []string{audience}, nil
|
||||
}
|
||||
|
||||
// GetIdentity implements auth.Provider.
|
||||
|
@ -118,7 +126,7 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
|
|||
Audience: audience,
|
||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||
TokenURL: "https://sts.googleapis.com/v1/token",
|
||||
SubjectTokenSupplier: TokenSupplier(oidcToken),
|
||||
SubjectTokenSupplier: StaticTokenSupplier(oidcToken),
|
||||
Scopes: scopes,
|
||||
}
|
||||
|
||||
|
@ -135,9 +143,7 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
|
|||
conf.TokenInfoURL = "https://sts.googleapis.com/v1/introspect"
|
||||
}
|
||||
|
||||
if hc := o.GetHTTPClient(); hc != nil {
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, hc)
|
||||
}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, o.GetHTTPClient())
|
||||
|
||||
src, err := p.impl().NewTokenSource(ctx, conf)
|
||||
if err != nil {
|
||||
|
@ -151,6 +157,12 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
|
|||
return &Token{*token}, nil
|
||||
}
|
||||
|
||||
// GetAccessTokenOptionsForArtifactRepository implements auth.Provider.
|
||||
func (Provider) GetAccessTokenOptionsForArtifactRepository(string) ([]auth.Option, error) {
|
||||
// GCP does not require any special options to retrieve access tokens.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
const registryPattern = `^(((.+\.)?gcr\.io)|(.+-docker\.pkg\.dev))$`
|
||||
|
||||
var registryRegex = regexp.MustCompile(registryPattern)
|
||||
|
@ -187,6 +199,85 @@ func (Provider) NewArtifactRegistryCredentials(_ context.Context, _ string,
|
|||
}, nil
|
||||
}
|
||||
|
||||
// GetAccessTokenOptionsForCluster implements auth.Provider.
|
||||
func (Provider) GetAccessTokenOptionsForCluster(opts ...auth.Option) ([][]auth.Option, error) {
|
||||
// A single token is needed. No options.
|
||||
return [][]auth.Option{{}}, nil
|
||||
}
|
||||
|
||||
// NewRESTConfig implements auth.Provider.
|
||||
func (p Provider) NewRESTConfig(ctx context.Context, accessTokens []auth.Token,
|
||||
opts ...auth.Option) (*auth.RESTConfig, error) {
|
||||
|
||||
token := accessTokens[0].(*Token)
|
||||
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
// Describe the cluster resource to get missing CA or endpoint.
|
||||
host := o.ClusterAddress
|
||||
caData := []byte(o.CAData)
|
||||
if host == "" || len(caData) == 0 {
|
||||
cluster := o.ClusterResource
|
||||
if err := parseCluster(cluster); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create client for describing the cluster resource.
|
||||
baseTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
if p := o.ProxyURL; p != nil {
|
||||
baseTransport.Proxy = http.ProxyURL(p)
|
||||
}
|
||||
transport, err := htransport.NewTransport(ctx, baseTransport, option.WithTokenSource(token.source()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create google http transport for describing GKE cluster: %w", err)
|
||||
}
|
||||
client, err := container.NewService(ctx, option.WithHTTPClient(&http.Client{Transport: transport}))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create client for describing GKE cluster: %w", err)
|
||||
}
|
||||
|
||||
// Describe the cluster resource.
|
||||
clusterResource, err := p.impl().GetCluster(ctx, cluster, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to describe GKE cluster '%s': %w", cluster, err)
|
||||
}
|
||||
|
||||
// Compare specified address and address from the cluster resource.
|
||||
endpoint := clusterResource.Endpoint
|
||||
if host != "" {
|
||||
canonicalAddress, err := auth.ParseClusterAddress(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse specified cluster address '%s': %w", host, err)
|
||||
}
|
||||
canonicalEndpoint, err := auth.ParseClusterAddress(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse GKE endpoint '%s': %w", endpoint, err)
|
||||
}
|
||||
if canonicalAddress != canonicalEndpoint {
|
||||
return nil, fmt.Errorf("GKE endpoint '%s' does not match specified address: '%s'", endpoint, host)
|
||||
}
|
||||
}
|
||||
|
||||
// Update host and CA with cluster details.
|
||||
host = endpoint
|
||||
if len(caData) == 0 {
|
||||
caData, err = base64.StdEncoding.DecodeString(clusterResource.MasterAuth.ClusterCaCertificate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode GKE CA certificate: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build and return the REST config.
|
||||
return &auth.RESTConfig{
|
||||
Host: host,
|
||||
BearerToken: token.AccessToken,
|
||||
CAData: caData,
|
||||
ExpiresAt: token.Expiry,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p Provider) impl() Implementation {
|
||||
if p.Implementation == nil {
|
||||
return implementation{}
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
. "github.com/onsi/gomega"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google/externalaccount"
|
||||
"google.golang.org/api/container/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
|
@ -33,7 +34,7 @@ import (
|
|||
"github.com/fluxcd/pkg/auth/gcp"
|
||||
)
|
||||
|
||||
func TestNewControllerToken(t *testing.T) {
|
||||
func TestProvider_NewControllerToken(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
impl := &mockImplementation{
|
||||
|
@ -72,7 +73,7 @@ func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
|||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
},
|
||||
SubjectTokenSupplier: gcp.TokenSupplier("oidc-token"),
|
||||
SubjectTokenSupplier: gcp.StaticTokenSupplier("oidc-token"),
|
||||
UniverseDomain: "googleapis.com",
|
||||
},
|
||||
},
|
||||
|
@ -87,7 +88,7 @@ func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
|||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
},
|
||||
SubjectTokenSupplier: gcp.TokenSupplier("oidc-token"),
|
||||
SubjectTokenSupplier: gcp.StaticTokenSupplier("oidc-token"),
|
||||
UniverseDomain: "googleapis.com",
|
||||
},
|
||||
annotations: map[string]string{
|
||||
|
@ -105,7 +106,7 @@ func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
|||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
},
|
||||
SubjectTokenSupplier: gcp.TokenSupplier("oidc-token"),
|
||||
SubjectTokenSupplier: gcp.StaticTokenSupplier("oidc-token"),
|
||||
UniverseDomain: "googleapis.com",
|
||||
},
|
||||
annotations: map[string]string{
|
||||
|
@ -123,7 +124,7 @@ func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
|||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
},
|
||||
SubjectTokenSupplier: gcp.TokenSupplier("oidc-token"),
|
||||
SubjectTokenSupplier: gcp.StaticTokenSupplier("oidc-token"),
|
||||
UniverseDomain: "googleapis.com",
|
||||
},
|
||||
annotations: map[string]string{
|
||||
|
@ -213,9 +214,9 @@ func TestProvider_GetAudience(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
aud, err := gcp.Provider{}.GetAudience(context.Background(), serviceAccount)
|
||||
aud, err := gcp.Provider{}.GetAudiences(context.Background(), serviceAccount)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(aud).To(Equal(tt.expected))
|
||||
g.Expect(aud).To(Equal([]string{tt.expected}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -259,12 +260,19 @@ func TestProvider_NewArtifactRegistryCredentials(t *testing.T) {
|
|||
|
||||
exp := time.Now()
|
||||
|
||||
accessToken := &gcp.Token{oauth2.Token{
|
||||
AccessToken: "access-token",
|
||||
Expiry: exp,
|
||||
}}
|
||||
provider := gcp.Provider{
|
||||
Implementation: &mockImplementation{
|
||||
t: t,
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
returnToken: &oauth2.Token{
|
||||
AccessToken: "access-token",
|
||||
Expiry: exp,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
creds, err := gcp.Provider{}.NewArtifactRegistryCredentials(context.Background(), "", accessToken)
|
||||
creds, err := auth.GetArtifactRegistryCredentials(context.Background(), provider, "gcr.io",
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}))
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(creds).NotTo(BeNil())
|
||||
g.Expect(creds.ExpiresAt).To(Equal(exp))
|
||||
|
@ -322,3 +330,131 @@ func TestProvider_ParseArtifactRegistry(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_NewRESTConfig(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
cluster string
|
||||
clusterAddress string
|
||||
caData string
|
||||
masterAuth *container.MasterAuth
|
||||
endpoint string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "valid GKE cluster",
|
||||
cluster: "projects/test-project/locations/us-central1/clusters/test-cluster",
|
||||
masterAuth: &container.MasterAuth{
|
||||
ClusterCaCertificate: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t", // base64 encoded "-----BEGIN CERTIFICATE-----"
|
||||
},
|
||||
endpoint: "https://203.0.113.10",
|
||||
},
|
||||
{
|
||||
name: "valid GKE cluster with address match",
|
||||
cluster: "projects/test-project/locations/us-central1/clusters/test-cluster",
|
||||
clusterAddress: "https://203.0.113.10:443",
|
||||
masterAuth: &container.MasterAuth{
|
||||
ClusterCaCertificate: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t",
|
||||
},
|
||||
endpoint: "https://203.0.113.10",
|
||||
},
|
||||
{
|
||||
name: "valid GKE cluster with CA",
|
||||
cluster: "projects/test-project/locations/us-central1/clusters/test-cluster",
|
||||
caData: "-----BEGIN CERTIFICATE-----",
|
||||
endpoint: "https://203.0.113.10",
|
||||
},
|
||||
{
|
||||
name: "CA and address only",
|
||||
clusterAddress: "https://203.0.113.10",
|
||||
caData: "-----BEGIN CERTIFICATE-----",
|
||||
endpoint: "https://203.0.113.10",
|
||||
},
|
||||
{
|
||||
name: "cluster address mismatch",
|
||||
cluster: "projects/test-project/locations/us-central1/clusters/test-cluster",
|
||||
clusterAddress: "https://198.51.100.10:443",
|
||||
masterAuth: &container.MasterAuth{
|
||||
ClusterCaCertificate: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t",
|
||||
},
|
||||
endpoint: "https://203.0.113.10",
|
||||
err: "GKE endpoint 'https://203.0.113.10' does not match specified address: 'https://198.51.100.10:443'",
|
||||
},
|
||||
{
|
||||
name: "invalid cluster ID",
|
||||
cluster: "invalid-cluster-id",
|
||||
err: "invalid GKE cluster ID: 'invalid-cluster-id'. must match ^projects/[^/]{1,200}/locations/[^/]{1,200}/clusters/[^/]{1,200}$",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tokenExpiry := time.Now().Add(1 * time.Hour)
|
||||
impl := &mockImplementation{
|
||||
t: t,
|
||||
expectGKEAPICall: tt.clusterAddress == "" || tt.caData == "",
|
||||
argCluster: tt.cluster,
|
||||
argProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com"},
|
||||
returnToken: &oauth2.Token{
|
||||
AccessToken: "access-token",
|
||||
Expiry: tokenExpiry,
|
||||
},
|
||||
returnCluster: &container.Cluster{
|
||||
Endpoint: tt.endpoint,
|
||||
MasterAuth: tt.masterAuth,
|
||||
},
|
||||
}
|
||||
|
||||
opts := []auth.Option{
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.example.com"}),
|
||||
}
|
||||
|
||||
if tt.cluster != "" {
|
||||
opts = append(opts, auth.WithClusterResource(tt.cluster))
|
||||
}
|
||||
|
||||
if tt.clusterAddress != "" {
|
||||
opts = append(opts, auth.WithClusterAddress(tt.clusterAddress))
|
||||
}
|
||||
|
||||
if tt.caData != "" {
|
||||
opts = append(opts, auth.WithCAData(tt.caData))
|
||||
}
|
||||
|
||||
provider := gcp.Provider{Implementation: impl}
|
||||
restConfig, err := auth.GetRESTConfig(context.Background(), provider, opts...)
|
||||
|
||||
if tt.err == "" {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(restConfig).NotTo(BeNil())
|
||||
g.Expect(restConfig.Host).To(Equal(tt.endpoint))
|
||||
g.Expect(restConfig.BearerToken).To(Equal("access-token"))
|
||||
g.Expect(restConfig.CAData).To(Equal([]byte("-----BEGIN CERTIFICATE-----")))
|
||||
g.Expect(restConfig.ExpiresAt).To(Equal(tokenExpiry))
|
||||
} else {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.err))
|
||||
g.Expect(restConfig).To(BeNil())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetAccessTokenOptionsForCluster(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
t.Run("with cluster resource", func(t *testing.T) {
|
||||
opts, err := gcp.Provider{}.GetAccessTokenOptionsForCluster(
|
||||
auth.WithClusterResource("projects/test-project/locations/us-central1/clusters/test-cluster"))
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(opts).To(HaveLen(1))
|
||||
g.Expect(opts[0]).To(HaveLen(0)) // Empty slice - no options needed for GCP
|
||||
})
|
||||
|
||||
t.Run("without cluster resource", func(t *testing.T) {
|
||||
opts, err := gcp.Provider{}.GetAccessTokenOptionsForCluster()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(opts).To(HaveLen(1))
|
||||
g.Expect(opts[0]).To(HaveLen(0)) // Empty slice - no options needed for GCP
|
||||
})
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ func (t *Token) GetDuration() time.Duration {
|
|||
return time.Until(t.Expiry)
|
||||
}
|
||||
|
||||
// Source gets a token source for the token to use with GCP libraries.
|
||||
func (t *Token) Source() oauth2.TokenSource {
|
||||
func (t *Token) source() oauth2.TokenSource {
|
||||
return oauth2.StaticTokenSource(&t.Token)
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ func NewTokenSource(ctx context.Context, opts ...auth.Option) oauth2.TokenSource
|
|||
|
||||
// Token implements oauth2.TokenSource.
|
||||
func (t *tokenSource) Token() (*oauth2.Token, error) {
|
||||
token, err := auth.GetToken(t.ctx, Provider{}, t.opts...)
|
||||
token, err := auth.GetAccessToken(t.ctx, Provider{}, t.opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -22,10 +22,10 @@ import (
|
|||
"golang.org/x/oauth2/google/externalaccount"
|
||||
)
|
||||
|
||||
// TokenSupplier provides a static OIDC token.
|
||||
type TokenSupplier string
|
||||
// StaticTokenSupplier provides a static OIDC token.
|
||||
type StaticTokenSupplier string
|
||||
|
||||
// SubjectToken implements externalaccount.SubjectTokenSupplier.
|
||||
func (s TokenSupplier) SubjectToken(context.Context, externalaccount.SupplierOptions) (string, error) {
|
||||
func (s StaticTokenSupplier) SubjectToken(context.Context, externalaccount.SupplierOptions) (string, error) {
|
||||
return string(s), nil
|
||||
}
|
||||
|
|
|
@ -14,13 +14,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authutils
|
||||
package generic
|
||||
|
||||
import "errors"
|
||||
import "os"
|
||||
|
||||
// ErrProviderDoesNotSupportRegistry is returned when the provider does not
|
||||
// support registry authentication.
|
||||
var ErrProviderDoesNotSupportRegistry = errors.New("provider does not support registry authentication")
|
||||
// Implementation provides the required methods of the generic libraries.
|
||||
type Implementation interface {
|
||||
ReadFile(name string) ([]byte, error)
|
||||
}
|
||||
|
||||
// ErrUnsupportedProvider is returned when the provider is not supported.
|
||||
var ErrUnsupportedProvider = errors.New("unsupported provider")
|
||||
type implementation struct{}
|
||||
|
||||
func (implementation) ReadFile(name string) ([]byte, error) {
|
||||
return os.ReadFile(name)
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2021 Stefan Prodan
|
||||
Copyright 2021 The Flux authors
|
||||
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.
|
||||
|
@ -15,19 +14,23 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ssa
|
||||
package generic_test
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"testing"
|
||||
|
||||
"github.com/fluxcd/pkg/ssa/normalize"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// SetNativeKindsDefaults sets default values for native Kubernetes objects,
|
||||
// working around various upstream Kubernetes API bugs.
|
||||
//
|
||||
// Deprecated: use normalize.UnstructuredList or normalize.Unstructured
|
||||
// instead.
|
||||
func SetNativeKindsDefaults(objects []*unstructured.Unstructured) error {
|
||||
return normalize.UnstructuredList(objects)
|
||||
type mockImplementation struct {
|
||||
t *testing.T
|
||||
|
||||
b []byte
|
||||
}
|
||||
|
||||
func (m *mockImplementation) ReadFile(name string) ([]byte, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(name).To(Equal("/var/run/secrets/kubernetes.io/serviceaccount/token"))
|
||||
return m.b, nil
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
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 generic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
authnv1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
)
|
||||
|
||||
// ProviderName is the name of the generic authentication provider.
|
||||
const ProviderName = "generic"
|
||||
|
||||
// Provider implements the auth.Provider interface for generic authentication.
|
||||
type Provider struct{ Implementation }
|
||||
|
||||
// GetName implements auth.RESTConfigProvider.
|
||||
func (p Provider) GetName() string {
|
||||
return ProviderName
|
||||
}
|
||||
|
||||
// NewControllerToken implements auth.RESTConfigProvider.
|
||||
func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (auth.Token, error) {
|
||||
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
if o.Client == nil {
|
||||
return nil, errors.New("client is required to create a controller token")
|
||||
}
|
||||
|
||||
// Like all providers, this one should fetch controller-level credentials
|
||||
// from the environment. In this case, this means opening the well-known
|
||||
// Kubernetes service account token file and parsing it to figure out
|
||||
// the controller's identity.
|
||||
const tokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||
b, err := p.impl().ReadFile(tokenFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read service account token file %s: %w", tokenFile, err)
|
||||
}
|
||||
|
||||
// Get controller service account from token subject.
|
||||
tok, _, err := jwt.NewParser().ParseUnverified(string(b), jwt.MapClaims{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse service account token: %w", err)
|
||||
}
|
||||
sub, err := tok.Claims.GetSubject()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get subject from service account token: %w", err)
|
||||
}
|
||||
parts := strings.Split(sub, ":")
|
||||
if len(parts) != 4 {
|
||||
return nil, fmt.Errorf("invalid subject format in service account token: %s", sub)
|
||||
}
|
||||
serviceAccount := corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: parts[3],
|
||||
Namespace: parts[2],
|
||||
},
|
||||
}
|
||||
|
||||
// Create token.
|
||||
tokenReq := &authnv1.TokenRequest{
|
||||
Spec: authnv1.TokenRequestSpec{
|
||||
Audiences: o.Audiences,
|
||||
},
|
||||
}
|
||||
if err := o.Client.SubResource("token").Create(ctx, &serviceAccount, tokenReq); err != nil {
|
||||
return nil, fmt.Errorf("failed to create kubernetes token for controller service account '%s': %w",
|
||||
client.ObjectKeyFromObject(&serviceAccount), err)
|
||||
}
|
||||
token := tokenReq.Status.Token
|
||||
|
||||
exp, err := getExpirationFromToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Token{
|
||||
Token: token,
|
||||
ExpiresAt: *exp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAudiences implements auth.RESTConfigProvider.
|
||||
func (Provider) GetAudiences(context.Context, corev1.ServiceAccount) ([]string, error) {
|
||||
// Use TokenRequest default audiences.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetIdentity implements auth.RESTConfigProvider.
|
||||
func (Provider) GetIdentity(serviceAccount corev1.ServiceAccount) (string, error) {
|
||||
return fmt.Sprintf("system:serviceaccount:%s:%s", serviceAccount.Namespace, serviceAccount.Name), nil
|
||||
}
|
||||
|
||||
// NewTokenForServiceAccount implements auth.RESTConfigProvider.
|
||||
func (Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken string,
|
||||
serviceAccount corev1.ServiceAccount, opts ...auth.Option) (auth.Token, error) {
|
||||
|
||||
exp, err := getExpirationFromToken(oidcToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Token{
|
||||
Token: oidcToken,
|
||||
ExpiresAt: *exp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAccessTokenOptionsForCluster implements auth.RESTConfigProvider.
|
||||
func (Provider) GetAccessTokenOptionsForCluster(opts ...auth.Option) ([][]auth.Option, error) {
|
||||
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
audiences := o.Audiences
|
||||
if len(audiences) == 0 {
|
||||
// Use cluster address as the default audience.
|
||||
audiences = []string{o.ClusterAddress}
|
||||
}
|
||||
|
||||
return [][]auth.Option{{auth.WithAudiences(audiences...)}}, nil
|
||||
}
|
||||
|
||||
// NewRESTConfig implements auth.RESTConfigProvider.
|
||||
func (Provider) NewRESTConfig(ctx context.Context, accessTokens []auth.Token,
|
||||
opts ...auth.Option) (*auth.RESTConfig, error) {
|
||||
|
||||
token := accessTokens[0].(*Token)
|
||||
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
// Parse the cluster address.
|
||||
host := o.ClusterAddress
|
||||
if host == "" {
|
||||
return nil, errors.New("cluster address is required to create a REST config")
|
||||
}
|
||||
var err error
|
||||
host, err = auth.ParseClusterAddress(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse cluster address %s: %w", o.ClusterAddress, err)
|
||||
}
|
||||
|
||||
// Get CA if provided.
|
||||
var caData []byte
|
||||
if o.CAData != "" {
|
||||
caData = []byte(o.CAData)
|
||||
}
|
||||
|
||||
return &auth.RESTConfig{
|
||||
Host: host,
|
||||
CAData: caData,
|
||||
BearerToken: token.Token,
|
||||
ExpiresAt: token.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p Provider) impl() Implementation {
|
||||
if p.Implementation == nil {
|
||||
return implementation{}
|
||||
}
|
||||
return p.Implementation
|
||||
}
|
||||
|
||||
func getExpirationFromToken(token string) (*time.Time, error) {
|
||||
tok, _, err := jwt.NewParser().ParseUnverified(token, jwt.MapClaims{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse service account token: %w", err)
|
||||
}
|
||||
exp, err := tok.Claims.GetExpirationTime()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get expiration time from service account token: %w", err)
|
||||
}
|
||||
return &exp.Time, nil
|
||||
}
|
|
@ -0,0 +1,349 @@
|
|||
/*
|
||||
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 generic_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
"github.com/fluxcd/pkg/auth/generic"
|
||||
"github.com/fluxcd/pkg/auth/utils"
|
||||
)
|
||||
|
||||
func TestProvider_NewControllerToken(t *testing.T) {
|
||||
t.Run("no client", func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
token, err := generic.Provider{}.NewControllerToken(context.Background())
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(Equal("client is required to create a controller token"))
|
||||
g.Expect(token).To(BeNil())
|
||||
})
|
||||
|
||||
t.Run("with audiences", func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
_, envClient, oidcClient := newTestEnv(t, ctx)
|
||||
|
||||
// Create service account.
|
||||
serviceAccount := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "controller",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
err := envClient.Create(ctx, serviceAccount)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create token.
|
||||
m := &mockImplementation{
|
||||
t: t,
|
||||
b: []byte("eyJhbGciOiJSUzI1NiIsImtpZCI6IkU2cUVmaVJ0QUY2OWhoNThZWU1QUmhPc1F1b1N5XzJuT1ZfRWF3TVRETlkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzUyMjkwMDE1LCJpYXQiOjE3NTIyODY0MTUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiMzEwMTgxZGItZDc3MC00MGE5LTg5MDEtN2M1NTQzOTBjZDhjIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImNvbnRyb2xsZXIiLCJ1aWQiOiJjMTUzNWEyNi01NDY5LTRmYzAtOGRiMi1kZWFhMGRlNDRmZjUifX0sIm5iZiI6MTc1MjI4NjQxNSwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6Y29udHJvbGxlciJ9.k-jt09bIwrGUNbSATEwaHHaaoym7NjcdStXcM0RYXZbL_PXCwP-TZPgBb2FzCq6V79E_q-NtZrY3RyvyAynUezXr6IPVkGne201uvOAjaibLvDxLzvbA5jWlZ0bHuLCfOxlC7GYSWjsglyH_ufulb6vxoMhY0rmiQzBbDHfB3EWM79-udcqLrxBsGgxjDnW4BXMIgSpuvipNA1GaMkpQb5AaY7Ns4zd0FftOimQmmvnwz8oDrGrCf2kmw91r0sAovva5B2BoJKlZwYGwO93zwTwK1qOMPLN2QHCUNBEY4K-QQlgz0oMUYR-YRpPJr7akjTQ6hm9zrTD90Tm0Jbqw7g\n"),
|
||||
}
|
||||
token, err := auth.GetAccessToken(ctx, generic.Provider{m},
|
||||
auth.WithClient(envClient),
|
||||
auth.WithAudiences("audience1", "audience2"))
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
genericToken := token.(*generic.Token)
|
||||
g.Expect(genericToken).NotTo(BeNil())
|
||||
|
||||
// Validate token.
|
||||
jwtToken, _, err := jwt.NewParser().ParseUnverified(genericToken.Token, jwt.MapClaims{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
sub, err := jwtToken.Claims.GetSubject()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(sub).To(Equal("system:serviceaccount:default:controller"))
|
||||
iss, err := jwtToken.Claims.GetIssuer()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
ctx = oidc.ClientContext(ctx, oidcClient)
|
||||
jwks := oidc.NewRemoteKeySet(ctx, iss+"openid/v1/jwks")
|
||||
for _, aud := range []string{"audience1", "audience2"} {
|
||||
_, err = oidc.NewVerifier(iss, jwks, &oidc.Config{
|
||||
ClientID: aud,
|
||||
SupportedSigningAlgs: []string{jwtToken.Method.Alg()},
|
||||
}).Verify(ctx, genericToken.Token)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
g.Expect(time.Until(genericToken.ExpiresAt)).To(BeNumerically("~", time.Hour, 10*time.Second))
|
||||
})
|
||||
}
|
||||
|
||||
func TestProvider_NewTokenForServiceAccount(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
_, envClient, oidcClient := newTestEnv(t, ctx)
|
||||
|
||||
t.Setenv(auth.EnvVarEnableObjectLevelWorkloadIdentity, "true")
|
||||
|
||||
// Create service account.
|
||||
serviceAccount := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
err := envClient.Create(ctx, serviceAccount)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create token.
|
||||
token, err := auth.GetAccessToken(ctx, generic.Provider{},
|
||||
auth.WithServiceAccount(client.ObjectKeyFromObject(serviceAccount), envClient),
|
||||
auth.WithAudiences("audience1", "audience2"))
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
genericToken := token.(*generic.Token)
|
||||
g.Expect(genericToken).NotTo(BeNil())
|
||||
|
||||
// Validate token.
|
||||
jwtToken, _, err := jwt.NewParser().ParseUnverified(genericToken.Token, jwt.MapClaims{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
sub, err := jwtToken.Claims.GetSubject()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(sub).To(Equal("system:serviceaccount:default:tenant"))
|
||||
iss, err := jwtToken.Claims.GetIssuer()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
ctx = oidc.ClientContext(ctx, oidcClient)
|
||||
jwks := oidc.NewRemoteKeySet(ctx, iss+"openid/v1/jwks")
|
||||
for _, aud := range []string{"audience1", "audience2"} {
|
||||
_, err = oidc.NewVerifier(iss, jwks, &oidc.Config{
|
||||
ClientID: aud,
|
||||
SupportedSigningAlgs: []string{jwtToken.Method.Alg()},
|
||||
}).Verify(ctx, genericToken.Token)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
g.Expect(time.Until(genericToken.ExpiresAt)).To(BeNumerically("~", time.Hour, 10*time.Second))
|
||||
}
|
||||
|
||||
func TestProvider_GetIdentity(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
id, err := generic.Provider{}.GetIdentity(corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant",
|
||||
Namespace: "default",
|
||||
},
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(id).To(Equal("system:serviceaccount:default:tenant"))
|
||||
}
|
||||
|
||||
func TestProvider_NewRESTConfig(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
_, envClient, oidcClient := newTestEnv(t, ctx)
|
||||
|
||||
// Create service account.
|
||||
serviceAccount := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "controller",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
err := envClient.Create(ctx, serviceAccount)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Mock implementation.
|
||||
m := &mockImplementation{
|
||||
t: t,
|
||||
b: []byte("eyJhbGciOiJSUzI1NiIsImtpZCI6IkU2cUVmaVJ0QUY2OWhoNThZWU1QUmhPc1F1b1N5XzJuT1ZfRWF3TVRETlkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzUyMjkwMDE1LCJpYXQiOjE3NTIyODY0MTUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiMzEwMTgxZGItZDc3MC00MGE5LTg5MDEtN2M1NTQzOTBjZDhjIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImNvbnRyb2xsZXIiLCJ1aWQiOiJjMTUzNWEyNi01NDY5LTRmYzAtOGRiMi1kZWFhMGRlNDRmZjUifX0sIm5iZiI6MTc1MjI4NjQxNSwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6Y29udHJvbGxlciJ9.k-jt09bIwrGUNbSATEwaHHaaoym7NjcdStXcM0RYXZbL_PXCwP-TZPgBb2FzCq6V79E_q-NtZrY3RyvyAynUezXr6IPVkGne201uvOAjaibLvDxLzvbA5jWlZ0bHuLCfOxlC7GYSWjsglyH_ufulb6vxoMhY0rmiQzBbDHfB3EWM79-udcqLrxBsGgxjDnW4BXMIgSpuvipNA1GaMkpQb5AaY7Ns4zd0FftOimQmmvnwz8oDrGrCf2kmw91r0sAovva5B2BoJKlZwYGwO93zwTwK1qOMPLN2QHCUNBEY4K-QQlgz0oMUYR-YRpPJr7akjTQ6hm9zrTD90Tm0Jbqw7g\n"),
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
audiences []string
|
||||
clusterAddress string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "address is required",
|
||||
err: "cluster address is required to create a REST config",
|
||||
},
|
||||
{
|
||||
name: "with audiences",
|
||||
clusterAddress: "https://example.com",
|
||||
audiences: []string{"audience1", "audience2"},
|
||||
},
|
||||
{
|
||||
name: "without audiences",
|
||||
clusterAddress: "https://example.com",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
opts := []auth.Option{
|
||||
auth.WithClient(envClient),
|
||||
auth.WithCAData("----- BEGIN CERTIFICATE-----"),
|
||||
}
|
||||
|
||||
if len(tt.audiences) > 0 {
|
||||
opts = append(opts, auth.WithAudiences(tt.audiences...))
|
||||
}
|
||||
|
||||
if tt.clusterAddress != "" {
|
||||
opts = append(opts, auth.WithClusterAddress(tt.clusterAddress))
|
||||
}
|
||||
|
||||
conf, err := auth.GetRESTConfig(ctx, generic.Provider{m}, opts...)
|
||||
|
||||
if tt.err != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(Equal(tt.err))
|
||||
g.Expect(conf).To(BeNil())
|
||||
return
|
||||
}
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(conf).NotTo(BeNil())
|
||||
|
||||
// Validate REST config.
|
||||
g.Expect(conf.Host).To(Equal("https://example.com:443"))
|
||||
g.Expect(conf.CAData).To(Equal([]byte("----- BEGIN CERTIFICATE-----")))
|
||||
g.Expect(time.Until(conf.ExpiresAt)).To(BeNumerically("~", time.Hour, 10*time.Second))
|
||||
|
||||
// Validate token.
|
||||
jwtToken, _, err := jwt.NewParser().ParseUnverified(conf.BearerToken, jwt.MapClaims{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
sub, err := jwtToken.Claims.GetSubject()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(sub).To(Equal("system:serviceaccount:default:controller"))
|
||||
iss, err := jwtToken.Claims.GetIssuer()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
ctx = oidc.ClientContext(ctx, oidcClient)
|
||||
jwks := oidc.NewRemoteKeySet(ctx, iss+"openid/v1/jwks")
|
||||
expectedAudiences := []string{"audience1", "audience2"}
|
||||
if len(tt.audiences) == 0 {
|
||||
expectedAudiences = []string{"https://example.com"}
|
||||
}
|
||||
for _, aud := range expectedAudiences {
|
||||
_, err = oidc.NewVerifier(iss, jwks, &oidc.Config{
|
||||
ClientID: aud,
|
||||
SupportedSigningAlgs: []string{jwtToken.Method.Alg()},
|
||||
}).Verify(ctx, conf.BearerToken)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_NewRESTConfig_EndToEnd(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
envConfig, envClient, _ := newTestEnv(t, ctx)
|
||||
|
||||
t.Setenv(auth.EnvVarEnableObjectLevelWorkloadIdentity, "true")
|
||||
|
||||
// Create service account.
|
||||
const (
|
||||
namespace = "default"
|
||||
saName = "tenant"
|
||||
cmName = "kubeconfig"
|
||||
)
|
||||
serviceAccount := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: saName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
g.Expect(envClient.Create(ctx, serviceAccount)).NotTo(HaveOccurred())
|
||||
|
||||
// Create kubeconfig configmap.
|
||||
kubeconfig := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: cmName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
meta.KubeConfigKeyProvider: generic.ProviderName,
|
||||
meta.KubeConfigKeyAddress: envConfig.Host,
|
||||
meta.KubeConfigKeyCACert: string(envConfig.CAData),
|
||||
meta.KubeConfigKeyServiceAccountName: saName,
|
||||
},
|
||||
}
|
||||
g.Expect(envClient.Create(ctx, kubeconfig)).NotTo(HaveOccurred())
|
||||
|
||||
// Create the authenticated client.
|
||||
fetcher := utils.GetRESTConfigFetcher(
|
||||
auth.WithClient(envClient),
|
||||
auth.WithClusterAddress(envConfig.Host),
|
||||
auth.WithCAData(string(envConfig.CAData)))
|
||||
conf, err := fetcher(ctx, meta.KubeConfigReference{
|
||||
ConfigMapRef: &meta.LocalObjectReference{
|
||||
Name: cmName,
|
||||
},
|
||||
}, namespace, envClient)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(conf).NotTo(BeNil())
|
||||
client, err := kubernetes.NewForConfig(conf)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Test a permission that an authenticated ServiceAccount should have.
|
||||
version, err := client.Discovery().ServerVersion()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(version).NotTo(BeNil())
|
||||
|
||||
// Test a permission that an authenticated ServiceAccount without any RBAC should NOT have.
|
||||
_, err = client.CoreV1().Namespaces().Get(ctx, "default", metav1.GetOptions{})
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(`forbidden: User "system:serviceaccount:default:tenant" cannot get resource "namespaces"`))
|
||||
}
|
||||
|
||||
func TestProvider_GetAccessTokenOptionsForCluster(t *testing.T) {
|
||||
t.Run("without audiences", func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
opts, err := generic.Provider{}.GetAccessTokenOptionsForCluster(
|
||||
auth.WithClusterAddress("https://example.com"))
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(opts).To(HaveLen(1))
|
||||
g.Expect(opts[0]).To(HaveLen(1))
|
||||
var o auth.Options
|
||||
o.Apply(opts[0]...)
|
||||
g.Expect(o.Audiences).To(ConsistOf("https://example.com"))
|
||||
})
|
||||
|
||||
t.Run("with audiences", func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
opts, err := generic.Provider{}.GetAccessTokenOptionsForCluster(
|
||||
auth.WithClusterAddress("https://example.com"),
|
||||
auth.WithAudiences("audience1", "audience2"))
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(opts).To(HaveLen(1))
|
||||
g.Expect(opts[0]).To(HaveLen(1))
|
||||
var o auth.Options
|
||||
o.Apply(opts[0]...)
|
||||
g.Expect(o.Audiences).To(ConsistOf("audience1", "audience2"))
|
||||
})
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
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 generic_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
)
|
||||
|
||||
func newTestEnv(t *testing.T, ctx context.Context) (*rest.Config, client.Client, *http.Client) {
|
||||
t.Helper()
|
||||
g := NewWithT(t)
|
||||
|
||||
// Create test env.
|
||||
testEnv := &envtest.Environment{}
|
||||
conf, err := testEnv.Start()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
t.Cleanup(func() { testEnv.Stop() })
|
||||
envClient, err := client.New(conf, client.Options{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create HTTP client for OIDC verification.
|
||||
clusterCAPool := x509.NewCertPool()
|
||||
ok := clusterCAPool.AppendCertsFromPEM(conf.TLSClientConfig.CAData)
|
||||
g.Expect(ok).To(BeTrue())
|
||||
oidcClient := &http.Client{}
|
||||
oidcClient.Transport = http.DefaultTransport.(*http.Transport).Clone()
|
||||
oidcClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||
RootCAs: clusterCAPool,
|
||||
}
|
||||
|
||||
// Grant anonymous access to service account issuer discovery.
|
||||
err = envClient.Create(ctx, &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "anonymous-service-account-issuer-discovery",
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "system:anonymous",
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "system:service-account-issuer-discovery",
|
||||
},
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return testEnv.Config, envClient, oidcClient
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
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 generic
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Token is the generic token.
|
||||
type Token struct {
|
||||
Token string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// GetDuration implements auth.Token.
|
||||
func (t *Token) GetDuration() time.Duration {
|
||||
return time.Until(t.ExpiresAt)
|
||||
}
|
|
@ -1,208 +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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
authnv1 "k8s.io/api/authentication/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/fluxcd/pkg/cache"
|
||||
)
|
||||
|
||||
// GetToken returns an access token for accessing resources in the given cloud provider.
|
||||
func GetToken(ctx context.Context, provider Provider, opts ...Option) (Token, error) {
|
||||
|
||||
var o Options
|
||||
o.Apply(opts...)
|
||||
|
||||
// Initialize access token fetcher for controller.
|
||||
newAccessToken := func() (Token, error) {
|
||||
token, err := provider.NewControllerToken(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create provider access token for the controller: %w", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Update access token fetcher for a service account if specified.
|
||||
var providerAudience string
|
||||
var providerIdentity string
|
||||
var serviceAccountP *corev1.ServiceAccount
|
||||
if o.ServiceAccount != nil {
|
||||
// Check the feature gate for object-level workload identity.
|
||||
if !IsObjectLevelWorkloadIdentityEnabled() {
|
||||
return nil, ErrObjectLevelWorkloadIdentityNotEnabled
|
||||
}
|
||||
|
||||
// Get service account and prepare a function to create a token for it.
|
||||
var serviceAccount corev1.ServiceAccount
|
||||
if err := o.Client.Get(ctx, *o.ServiceAccount, &serviceAccount); err != nil {
|
||||
return nil, fmt.Errorf("failed to get service account '%s/%s': %w",
|
||||
o.ServiceAccount.Namespace, o.ServiceAccount.Name, err)
|
||||
}
|
||||
serviceAccountP = &serviceAccount
|
||||
|
||||
// Get provider audience.
|
||||
var err error
|
||||
providerAudience, err = provider.GetAudience(ctx, serviceAccount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get provider audience: %w", err)
|
||||
}
|
||||
|
||||
// Get provider identity.
|
||||
providerIdentity, err = provider.GetIdentity(serviceAccount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get provider identity from service account '%s/%s' annotations: %w",
|
||||
serviceAccount.Namespace, serviceAccount.Name, err)
|
||||
}
|
||||
|
||||
// Update access token fetcher.
|
||||
newAccessToken = func() (Token, error) {
|
||||
identityToken, err := newServiceAccountToken(ctx, o.Client, serviceAccount, providerAudience)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create kubernetes token for service account '%s/%s': %w",
|
||||
serviceAccount.Namespace, serviceAccount.Name, err)
|
||||
}
|
||||
|
||||
token, err := provider.NewTokenForServiceAccount(ctx, identityToken, serviceAccount, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create provider access token for service account '%s/%s': %w",
|
||||
serviceAccount.Namespace, serviceAccount.Name, err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize token fetcher with access token fetcher.
|
||||
newToken := newAccessToken
|
||||
|
||||
// Update token fetcher to registry token fetcher if artifact repository is specified.
|
||||
var artifactRepositoryCacheKey string
|
||||
if o.ArtifactRepository != "" {
|
||||
// Parse artifact repository.
|
||||
registryInput, err := provider.ParseArtifactRepository(o.ArtifactRepository)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse artifact repository '%s': %w",
|
||||
o.ArtifactRepository, err)
|
||||
}
|
||||
|
||||
// Set artifact repository cache key.
|
||||
artifactRepositoryCacheKey = registryInput
|
||||
|
||||
// Update token fetcher.
|
||||
newToken = func() (Token, error) {
|
||||
accessToken, err := newAccessToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := provider.NewArtifactRegistryCredentials(ctx, registryInput, accessToken, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create artifact registry credentials: %w", err)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Bail out early if cache is disabled.
|
||||
if o.Cache == nil {
|
||||
return newToken()
|
||||
}
|
||||
|
||||
// Build cache key.
|
||||
cacheKey := buildCacheKey(provider, providerAudience, providerIdentity,
|
||||
artifactRepositoryCacheKey, serviceAccountP, opts...)
|
||||
|
||||
// Get involved object details.
|
||||
kind := o.InvolvedObject.Kind
|
||||
name := o.InvolvedObject.Name
|
||||
namespace := o.InvolvedObject.Namespace
|
||||
operation := o.InvolvedObject.Operation
|
||||
|
||||
// Get token from cache.
|
||||
token, _, err := o.Cache.GetOrSet(ctx, cacheKey, func(ctx context.Context) (cache.Token, error) {
|
||||
return newToken()
|
||||
}, cache.WithInvolvedObject(kind, name, namespace, operation))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func newServiceAccountToken(ctx context.Context, client client.Client,
|
||||
serviceAccount corev1.ServiceAccount, providerAudience string) (string, error) {
|
||||
tokenReq := &authnv1.TokenRequest{
|
||||
Spec: authnv1.TokenRequestSpec{
|
||||
Audiences: []string{providerAudience},
|
||||
},
|
||||
}
|
||||
if err := client.SubResource("token").Create(ctx, &serviceAccount, tokenReq); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return tokenReq.Status.Token, nil
|
||||
}
|
||||
|
||||
func buildCacheKey(provider Provider, providerAudience, providerIdentity, artifactRepositoryKey string,
|
||||
serviceAccount *corev1.ServiceAccount, opts ...Option) string {
|
||||
|
||||
var o Options
|
||||
o.Apply(opts...)
|
||||
|
||||
var keyParts []string
|
||||
|
||||
keyParts = append(keyParts, fmt.Sprintf("provider=%s", provider.GetName()))
|
||||
|
||||
if serviceAccount != nil {
|
||||
keyParts = append(keyParts, fmt.Sprintf("providerAudience=%s", providerAudience))
|
||||
keyParts = append(keyParts, fmt.Sprintf("providerIdentity=%s", providerIdentity))
|
||||
keyParts = append(keyParts, fmt.Sprintf("serviceAccountName=%s", serviceAccount.Name))
|
||||
keyParts = append(keyParts, fmt.Sprintf("serviceAccountNamespace=%s", serviceAccount.Namespace))
|
||||
}
|
||||
|
||||
if len(o.Scopes) > 0 {
|
||||
keyParts = append(keyParts, fmt.Sprintf("scopes=%s", strings.Join(o.Scopes, ",")))
|
||||
}
|
||||
|
||||
if o.ArtifactRepository != "" {
|
||||
keyParts = append(keyParts, fmt.Sprintf("artifactRepositoryKey=%s", artifactRepositoryKey))
|
||||
}
|
||||
|
||||
if o.STSRegion != "" {
|
||||
keyParts = append(keyParts, fmt.Sprintf("stsRegion=%s", o.STSRegion))
|
||||
}
|
||||
|
||||
if o.STSEndpoint != "" {
|
||||
keyParts = append(keyParts, fmt.Sprintf("stsEndpoint=%s", o.STSEndpoint))
|
||||
}
|
||||
|
||||
if o.ProxyURL != nil {
|
||||
keyParts = append(keyParts, fmt.Sprintf("proxyURL=%s", o.ProxyURL.String()))
|
||||
}
|
||||
|
||||
s := strings.Join(keyParts, ",")
|
||||
hash := sha256.Sum256([]byte(s))
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
|
@ -1,428 +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 auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
"github.com/fluxcd/pkg/cache"
|
||||
)
|
||||
|
||||
type mockToken struct {
|
||||
token string
|
||||
}
|
||||
|
||||
func (m *mockToken) GetDuration() time.Duration {
|
||||
return time.Hour
|
||||
}
|
||||
|
||||
type mockProvider struct {
|
||||
t *testing.T
|
||||
|
||||
returnName string
|
||||
returnAudience string
|
||||
returnIdentity string
|
||||
returnIdentityErr string
|
||||
returnRegistryErr string
|
||||
returnRegistryInput string
|
||||
returnControllerToken auth.Token
|
||||
returnAccessToken auth.Token
|
||||
returnRegistryToken *auth.ArtifactRegistryCredentials
|
||||
paramServiceAccount corev1.ServiceAccount
|
||||
paramOIDCTokenClient *http.Client
|
||||
paramArtifactRepository string
|
||||
paramAccessToken auth.Token
|
||||
paramAllowShellOut bool
|
||||
}
|
||||
|
||||
func (m *mockProvider) GetName() string {
|
||||
return m.returnName
|
||||
}
|
||||
|
||||
func (m *mockProvider) NewControllerToken(ctx context.Context, opts ...auth.Option) (auth.Token, error) {
|
||||
checkOptions(m.t, m.paramAllowShellOut, opts...)
|
||||
return m.returnControllerToken, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) GetAudience(ctx context.Context, serviceAccount corev1.ServiceAccount) (string, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(serviceAccount).To(Equal(m.paramServiceAccount))
|
||||
return m.returnAudience, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) GetIdentity(serviceAccount corev1.ServiceAccount) (string, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(serviceAccount).To(Equal(m.paramServiceAccount))
|
||||
if m.returnIdentityErr != "" {
|
||||
return "", errors.New(m.returnIdentityErr)
|
||||
}
|
||||
return m.returnIdentity, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) NewTokenForServiceAccount(ctx context.Context, oidcToken string,
|
||||
serviceAccount corev1.ServiceAccount, opts ...auth.Option) (auth.Token, error) {
|
||||
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
|
||||
// Verify the OIDC token.
|
||||
g.Expect(m.returnAudience).NotTo(BeEmpty())
|
||||
token, _, err := jwt.NewParser().ParseUnverified(oidcToken, jwt.MapClaims{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
iss, err := token.Claims.GetIssuer()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
ctx = oidc.ClientContext(ctx, m.paramOIDCTokenClient)
|
||||
jwks := oidc.NewRemoteKeySet(ctx, iss+"openid/v1/jwks")
|
||||
_, err = oidc.NewVerifier(iss, jwks, &oidc.Config{
|
||||
ClientID: m.returnAudience,
|
||||
SupportedSigningAlgs: []string{token.Method.Alg()},
|
||||
}).Verify(ctx, oidcToken)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
g.Expect(serviceAccount).To(Equal(m.paramServiceAccount))
|
||||
|
||||
checkOptions(m.t, m.paramAllowShellOut, opts...)
|
||||
|
||||
return m.returnAccessToken, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) ParseArtifactRepository(artifactRepository string) (string, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(artifactRepository).To(Equal(m.paramArtifactRepository))
|
||||
if m.returnRegistryErr != "" {
|
||||
return "", errors.New(m.returnRegistryErr)
|
||||
}
|
||||
return m.returnRegistryInput, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) NewArtifactRegistryCredentials(ctx context.Context, registryInput string,
|
||||
accessToken auth.Token, opts ...auth.Option) (*auth.ArtifactRegistryCredentials, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(registryInput).To(Equal(m.paramArtifactRepository))
|
||||
g.Expect(accessToken).To(Equal(m.paramAccessToken))
|
||||
checkOptions(m.t, m.paramAllowShellOut, opts...)
|
||||
return m.returnRegistryToken, nil
|
||||
}
|
||||
|
||||
func checkOptions(t *testing.T, allowShellOut bool, opts ...auth.Option) {
|
||||
t.Helper()
|
||||
g := NewWithT(t)
|
||||
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
g.Expect(o.Scopes).To(Equal([]string{"scope1", "scope2"}))
|
||||
g.Expect(o.STSRegion).To(Equal("us-east-1"))
|
||||
g.Expect(o.STSEndpoint).To(Equal("https://sts.some-cloud.io"))
|
||||
g.Expect(o.ProxyURL).To(Equal(&url.URL{Scheme: "http", Host: "proxy.io:8080"}))
|
||||
g.Expect(o.AllowShellOut).To(Equal(allowShellOut))
|
||||
}
|
||||
|
||||
func TestGetToken(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
// Create test env.
|
||||
testEnv := &envtest.Environment{}
|
||||
conf, err := testEnv.Start()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
t.Cleanup(func() { testEnv.Stop() })
|
||||
kubeClient, err := client.New(conf, client.Options{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create HTTP client for OIDC verification.
|
||||
clusterCAPool := x509.NewCertPool()
|
||||
ok := clusterCAPool.AppendCertsFromPEM(conf.TLSClientConfig.CAData)
|
||||
g.Expect(ok).To(BeTrue())
|
||||
oidcClient := &http.Client{}
|
||||
oidcClient.Transport = http.DefaultTransport.(*http.Transport).Clone()
|
||||
oidcClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||
RootCAs: clusterCAPool,
|
||||
}
|
||||
|
||||
// Grant anonymous access to service account issuer discovery.
|
||||
err = kubeClient.Create(ctx, &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "anonymous-service-account-issuer-discovery",
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "system:anonymous",
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "system:service-account-issuer-discovery",
|
||||
},
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create a default service account.
|
||||
defaultServiceAccount := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
err = kubeClient.Create(ctx, defaultServiceAccount)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
saRef := client.ObjectKey{
|
||||
Name: defaultServiceAccount.Name,
|
||||
Namespace: defaultServiceAccount.Namespace,
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
provider *mockProvider
|
||||
opts []auth.Option
|
||||
disableObjectLevel bool
|
||||
expectedToken auth.Token
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "controller access token",
|
||||
provider: &mockProvider{
|
||||
returnControllerToken: &mockToken{token: "mock-default-token"},
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
},
|
||||
expectedToken: &mockToken{token: "mock-default-token"},
|
||||
},
|
||||
{
|
||||
name: "controller access token allowing shell out",
|
||||
provider: &mockProvider{
|
||||
returnControllerToken: &mockToken{token: "mock-default-token"},
|
||||
paramAllowShellOut: true,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithAllowShellOut(),
|
||||
},
|
||||
expectedToken: &mockToken{token: "mock-default-token"},
|
||||
},
|
||||
{
|
||||
name: "registry token from controller access token",
|
||||
provider: &mockProvider{
|
||||
returnRegistryInput: "some-registry.io/some/artifact",
|
||||
returnControllerToken: &mockToken{token: "mock-default-token"},
|
||||
returnRegistryToken: &auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}),
|
||||
},
|
||||
paramAccessToken: &mockToken{token: "mock-default-token"},
|
||||
paramArtifactRepository: "some-registry.io/some/artifact",
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithArtifactRepository("some-registry.io/some/artifact"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
},
|
||||
expectedToken: &auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "access token from service account",
|
||||
provider: &mockProvider{
|
||||
returnName: "mock-provider",
|
||||
returnAudience: "mock-audience",
|
||||
returnAccessToken: &mockToken{token: "mock-access-token"},
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
paramOIDCTokenClient: oidcClient,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
// Exercise the code path where a cache is set but no token is
|
||||
// available in the cache.
|
||||
func(o *auth.Options) {
|
||||
tokenCache, err := cache.NewTokenCache(1)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
o.Cache = tokenCache
|
||||
},
|
||||
},
|
||||
expectedToken: &mockToken{token: "mock-access-token"},
|
||||
},
|
||||
{
|
||||
name: "registry token from access token from service account",
|
||||
provider: &mockProvider{
|
||||
returnName: "mock-provider",
|
||||
returnAudience: "mock-audience",
|
||||
returnRegistryInput: "some-registry.io/some/artifact",
|
||||
returnAccessToken: &mockToken{token: "mock-access-token"},
|
||||
returnRegistryToken: &auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}),
|
||||
},
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
paramOIDCTokenClient: oidcClient,
|
||||
paramArtifactRepository: "some-registry.io/some/artifact",
|
||||
paramAccessToken: &mockToken{token: "mock-access-token"},
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithArtifactRepository("some-registry.io/some/artifact"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
},
|
||||
expectedToken: &auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all the options are taken into account in the cache key",
|
||||
provider: &mockProvider{
|
||||
returnName: "mock-provider",
|
||||
returnAudience: "mock-audience",
|
||||
returnIdentity: "mock-identity",
|
||||
returnRegistryInput: "artifact-cache-key",
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
paramArtifactRepository: "some-registry.io/some/artifact",
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithArtifactRepository("some-registry.io/some/artifact"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
func(o *auth.Options) {
|
||||
tokenCache, err := cache.NewTokenCache(1)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
const key = "3e8e270134e99fda1a01d7dca77f29448eb4c7f6cc026137b85a1bcd96b276fa"
|
||||
token := &mockToken{token: "cached-token"}
|
||||
cachedToken, ok, err := tokenCache.GetOrSet(ctx, key, func(ctx context.Context) (cache.Token, error) {
|
||||
return token, nil
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(ok).To(BeFalse())
|
||||
g.Expect(cachedToken).To(Equal(token))
|
||||
|
||||
o.Cache = tokenCache
|
||||
},
|
||||
},
|
||||
expectedToken: &mockToken{token: "cached-token"},
|
||||
},
|
||||
{
|
||||
name: "error getting identity",
|
||||
provider: &mockProvider{
|
||||
returnIdentityErr: "mock error",
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
},
|
||||
expectedErr: "failed to get provider identity from service account 'default/default' annotations: mock error",
|
||||
},
|
||||
{
|
||||
name: "error getting identity using cache",
|
||||
provider: &mockProvider{
|
||||
returnIdentityErr: "mock error",
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
func(o *auth.Options) {
|
||||
tokenCache, err := cache.NewTokenCache(1)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
o.Cache = tokenCache
|
||||
},
|
||||
},
|
||||
expectedErr: "failed to get provider identity from service account 'default/default' annotations: mock error",
|
||||
},
|
||||
{
|
||||
name: "error parsing artifact repository",
|
||||
provider: &mockProvider{
|
||||
paramArtifactRepository: "some-registry.io/some/artifact",
|
||||
returnRegistryErr: "mock error",
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithArtifactRepository("some-registry.io/some/artifact"),
|
||||
},
|
||||
expectedErr: "failed to parse artifact repository 'some-registry.io/some/artifact': mock error",
|
||||
},
|
||||
{
|
||||
name: "disable object level workload identity",
|
||||
provider: &mockProvider{},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
},
|
||||
disableObjectLevel: true,
|
||||
expectedErr: "ObjectLevelWorkloadIdentity feature gate is not enabled",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tt.provider.t = t
|
||||
|
||||
if !tt.disableObjectLevel {
|
||||
t.Setenv(auth.EnvVarEnableObjectLevelWorkloadIdentity, "true")
|
||||
}
|
||||
|
||||
token, err := auth.GetToken(ctx, tt.provider, tt.opts...)
|
||||
|
||||
if tt.expectedErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(Equal(tt.expectedErr))
|
||||
} else {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(token).To(Equal(tt.expectedToken))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
93
auth/go.mod
93
auth/go.mod
|
@ -2,59 +2,76 @@ module github.com/fluxcd/pkg/auth
|
|||
|
||||
go 1.24.0
|
||||
|
||||
replace github.com/fluxcd/pkg/cache => ../cache
|
||||
replace (
|
||||
github.com/fluxcd/pkg/apis/meta => ../apis/meta
|
||||
github.com/fluxcd/pkg/cache => ../cache
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.6.0
|
||||
cloud.google.com/go/compute/metadata v0.7.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.0
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19
|
||||
github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.45.1
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.2
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.66.1
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/fluxcd/pkg/cache v0.9.0
|
||||
github.com/fluxcd/pkg/apis/meta v1.18.0
|
||||
github.com/fluxcd/pkg/cache v0.10.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/go-containerregistry v0.20.3
|
||||
github.com/google/go-containerregistry v0.20.6
|
||||
github.com/onsi/gomega v1.37.0
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
k8s.io/api v0.33.0
|
||||
k8s.io/apimachinery v0.33.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
google.golang.org/api v0.241.0
|
||||
k8s.io/api v0.33.2
|
||||
k8s.io/apimachinery v0.33.2
|
||||
k8s.io/client-go v0.33.2
|
||||
sigs.k8s.io/controller-runtime v0.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.16.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
||||
github.com/aws/smithy-go v1.22.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
|
||||
github.com/aws/smithy-go v1.22.4 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/docker/cli v27.5.0+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.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.11.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gnostic-models v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
|
@ -73,23 +90,29 @@ require (
|
|||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/term v0.33.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // 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
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.33.0 // indirect
|
||||
k8s.io/client-go v0.33.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.33.2 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
|
||||
sigs.k8s.io/yaml v1.5.0 // indirect
|
||||
)
|
||||
|
|
188
auth/go.sum
188
auth/go.sum
|
@ -1,47 +1,57 @@
|
|||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/auth v0.16.2 h1:QvBAGFPLrDeoiNjyfVunhQ10HKNYuOwZ5noee0M5df4=
|
||||
cloud.google.com/go/auth v0.16.2/go.mod h1:sRBas2Y1fB1vZTdurouM0AzuYQBMZinrUYL8EufhtEA=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
|
||||
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 h1:ldKsKtEIblsgsr6mPwrd9yRntoX6uLz/K89wsldwx/k=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3/go.mod h1:MAm7bk0oDLmD8yIkvfbxPW04fxzphPyL+7GzwHxOp6Y=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0 h1:figxyQZXzZQIcP3njhC68bYUiTw45J8/SsHaLW8Ax0M=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice v1.0.0/go.mod h1:TmlMW4W5OvXOmOyKNnor8nlMMiO1ctIyzmHme/VHsrA=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 h1:YyH8Hk73bYzdbvf6S8NF5z/fb/1stpiMnFSfL6jSfRA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.0 h1:wA2O6pZ2r5smqJunFP4hp7qptMW4EQxs8O6RVHPulOE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.0/go.mod h1:RZL7ov7c72wSmoM8bIiVxRHgcVdzhNkVW2J36C8RF4s=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
|
||||
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
||||
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.45.1 h1:Bwzh202Aq7/MYnAjXA9VawCf6u+hjwMdoYmZ4HYsdf8=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.45.1/go.mod h1:xZzWl9AXYa6zsLLH41HBFW8KRKJRIzlGmvSM0mVMIX4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.2 h1:XJ/AEFYj9VFPJdF+VFi4SUPEDfz1akHwxxm07JfZJcs=
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.33.2/go.mod h1:JUBHdhvKbbKmhaHjLsKJAWnQL80T6nURmhB/LEprV+4=
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.66.1 h1:sD1y3G4WXw1GjK95L5dBXPFXNWl/O8GMradUojUYqCg=
|
||||
github.com/aws/aws-sdk-go-v2/service/eks v1.66.1/go.mod h1:Qj90srO2HigGG5x8Ro6RxixxqiSjZjF91WTEVpnsjAs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
|
||||
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
|
||||
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
|
@ -57,22 +67,27 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
|||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
|
||||
github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
|
||||
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
|
@ -89,18 +104,26 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
|||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
|
||||
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
|
||||
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
|
||||
github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU=
|
||||
github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
|
||||
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
|
@ -173,57 +196,87 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
|||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
|
||||
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
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/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/api v0.241.0 h1:QKwqWQlkc6O895LchPEDUSYr22Xp3NCxpQRiWTB6avE=
|
||||
google.golang.org/api v0.241.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
|
||||
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
@ -236,14 +289,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
|
||||
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
|
||||
k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
|
||||
k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
|
||||
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
||||
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
|
||||
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
|
||||
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
|
||||
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
|
||||
k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8=
|
||||
k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8=
|
||||
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
|
||||
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
|
||||
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
|
||||
|
@ -257,7 +310,8 @@ sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8
|
|||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/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=
|
||||
|
|
102
auth/options.go
102
auth/options.go
|
@ -19,6 +19,7 @@ package auth
|
|||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
|
@ -31,25 +32,35 @@ type Option func(*Options)
|
|||
// Options contains options for configuring the behavior of the provider methods.
|
||||
// Not all providers/methods support all options.
|
||||
type Options struct {
|
||||
Client client.Client
|
||||
Cache *cache.TokenCache
|
||||
ServiceAccount *client.ObjectKey
|
||||
InvolvedObject cache.InvolvedObject
|
||||
Scopes []string
|
||||
ArtifactRepository string
|
||||
STSRegion string
|
||||
STSEndpoint string
|
||||
ProxyURL *url.URL
|
||||
AllowShellOut bool
|
||||
Client client.Client
|
||||
Cache *cache.TokenCache
|
||||
ServiceAccount *client.ObjectKey
|
||||
InvolvedObject cache.InvolvedObject
|
||||
Audiences []string
|
||||
Scopes []string
|
||||
STSRegion string
|
||||
STSEndpoint string
|
||||
ProxyURL *url.URL
|
||||
CAData string
|
||||
ClusterResource string
|
||||
ClusterAddress string
|
||||
AllowShellOut bool
|
||||
}
|
||||
|
||||
// WithClient sets the controller-runtime client for the provider.
|
||||
func WithClient(client client.Client) Option {
|
||||
return func(o *Options) {
|
||||
o.Client = client
|
||||
}
|
||||
}
|
||||
|
||||
// WithServiceAccount sets the ServiceAccount reference for the token
|
||||
// and a controller-runtime client to fetch the ServiceAccount and
|
||||
// create an OIDC token for it in the Kubernetes API.
|
||||
func WithServiceAccount(saRef client.ObjectKey, client client.Client) Option {
|
||||
func WithServiceAccount(saRef client.ObjectKey, c client.Client) Option {
|
||||
return func(o *Options) {
|
||||
o.ServiceAccount = &saRef
|
||||
o.Client = client
|
||||
o.Client = c
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,6 +72,13 @@ func WithCache(cache cache.TokenCache, involvedObject cache.InvolvedObject) Opti
|
|||
}
|
||||
}
|
||||
|
||||
// WithAudiences sets the audiences for the Kubernetes ServiceAccount token.
|
||||
func WithAudiences(audiences ...string) Option {
|
||||
return func(o *Options) {
|
||||
o.Audiences = audiences
|
||||
}
|
||||
}
|
||||
|
||||
// WithScopes sets the scopes for the token.
|
||||
func WithScopes(scopes ...string) Option {
|
||||
return func(o *Options) {
|
||||
|
@ -68,16 +86,6 @@ func WithScopes(scopes ...string) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithArtifactRepository sets the artifact repository the token will be used for.
|
||||
// In most cases artifact registry credentials require an additional
|
||||
// token exchange at the end. This option allows the library to implement
|
||||
// this exchange and cache the final token.
|
||||
func WithArtifactRepository(artifactRepository string) Option {
|
||||
return func(o *Options) {
|
||||
o.ArtifactRepository = artifactRepository
|
||||
}
|
||||
}
|
||||
|
||||
// WithSTSRegion sets the region for the STS service (some cloud providers
|
||||
// require a region, e.g. AWS).
|
||||
func WithSTSRegion(stsRegion string) Option {
|
||||
|
@ -100,7 +108,38 @@ func WithProxyURL(proxyURL url.URL) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithAllowShellOut allows the provider to shell out to binaries.
|
||||
// WithCAData sets the CA data for credentials that require a CA,
|
||||
// e.g. for Kubernetes REST config.
|
||||
func WithCAData(caData string) Option {
|
||||
return func(o *Options) {
|
||||
o.CAData = caData
|
||||
}
|
||||
}
|
||||
|
||||
// WithClusterResource sets the cluster resource for creating a REST config.
|
||||
// Must be the fully qualified name of the cluster resource in the cloud
|
||||
// provider API.
|
||||
func WithClusterResource(clusterResource string) Option {
|
||||
return func(o *Options) {
|
||||
o.ClusterResource = clusterResource
|
||||
}
|
||||
}
|
||||
|
||||
// WithClusterAddress sets the cluster address for creating a REST config.
|
||||
// This address is used to select the correct cluster endpoint and CA data
|
||||
// when the provider has a list of endpoints to choose from, or to simply
|
||||
// validate the address against the cluster resource when the provider
|
||||
// returns a single endpoint. This is optional, providers returning a list
|
||||
// of endpoints will select the first one if no address is provided.
|
||||
func WithClusterAddress(clusterAddress string) Option {
|
||||
return func(o *Options) {
|
||||
o.ClusterAddress = clusterAddress
|
||||
}
|
||||
}
|
||||
|
||||
// WithAllowShellOut allows the provider to shell out to binary tools
|
||||
// for acquiring controller tokens. MUST be used only by the Flux CLI,
|
||||
// i.e. in the github.com/fluxcd/flux2 Git repository.
|
||||
func WithAllowShellOut() Option {
|
||||
return func(o *Options) {
|
||||
o.AllowShellOut = true
|
||||
|
@ -114,14 +153,17 @@ func (o *Options) Apply(opts ...Option) {
|
|||
}
|
||||
}
|
||||
|
||||
// GetHTTPClient returns a *http.Client with the configured proxy URL
|
||||
// or nil if no proxy URL is set.
|
||||
// GetHTTPClient returns a *http.Client with appropriate timeouts and proxy settings.
|
||||
// The client includes a 10-second timeout to prevent indefinite hangs during token acquisition.
|
||||
func (o *Options) GetHTTPClient() *http.Client {
|
||||
if o.ProxyURL == nil {
|
||||
return nil
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
if o.ProxyURL != nil {
|
||||
transport.Proxy = http.ProxyURL(o.ProxyURL)
|
||||
}
|
||||
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.Proxy = http.ProxyURL(o.ProxyURL)
|
||||
return &http.Client{Transport: transport}
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestOptions_GetHTTPClient(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
options Options
|
||||
expectProxy bool
|
||||
}{
|
||||
{
|
||||
name: "no proxy configured",
|
||||
options: Options{},
|
||||
expectProxy: false,
|
||||
},
|
||||
{
|
||||
name: "proxy configured",
|
||||
options: Options{
|
||||
ProxyURL: &url.URL{Scheme: "http", Host: "proxy.example.com:8080"},
|
||||
},
|
||||
expectProxy: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := tt.options.GetHTTPClient()
|
||||
|
||||
if client == nil {
|
||||
t.Error("GetHTTPClient() returned nil, expected non-nil client")
|
||||
}
|
||||
|
||||
expectedTimeout := 10 * time.Second
|
||||
if client.Timeout != expectedTimeout {
|
||||
t.Errorf("GetHTTPClient() timeout = %v, want %v", client.Timeout, expectedTimeout)
|
||||
}
|
||||
|
||||
if client.Transport == nil {
|
||||
t.Error("GetHTTPClient() transport is nil")
|
||||
return
|
||||
}
|
||||
|
||||
if tt.expectProxy && tt.options.ProxyURL == nil {
|
||||
t.Error("Expected proxy but ProxyURL is nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -22,10 +22,10 @@ import (
|
|||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// Provider contains the logic to retrieve an access token for a cloud
|
||||
// provider from a ServiceAccount (OIDC/JWT) token.
|
||||
// Provider contains the logic to retrieve security credentials
|
||||
// for accessing resources in a cloud provider.
|
||||
type Provider interface {
|
||||
// GetName returns the name of the provider.
|
||||
// GetName returns the name of the cloud provider.
|
||||
GetName() string
|
||||
|
||||
// NewControllerToken returns a token that can be used to authenticate
|
||||
|
@ -34,11 +34,11 @@ type Provider interface {
|
|||
// environment variables, local metadata services, etc.
|
||||
NewControllerToken(ctx context.Context, opts ...Option) (Token, error)
|
||||
|
||||
// GetAudience returns the audience the OIDC tokens issued representing
|
||||
// ServiceAccounts should have. This is usually a string that represents
|
||||
// GetAudiences returns the audiences the OIDC tokens issued representing
|
||||
// ServiceAccounts should have. These are usually strings that represent
|
||||
// the cloud provider's STS service, or some entity in the provider for
|
||||
// which the OIDC tokens are targeted to.
|
||||
GetAudience(ctx context.Context, serviceAccount corev1.ServiceAccount) (string, error)
|
||||
GetAudiences(ctx context.Context, serviceAccount corev1.ServiceAccount) ([]string, error)
|
||||
|
||||
// GetIdentity takes a ServiceAccount and returns the identity which the
|
||||
// ServiceAccount wants to impersonate, by looking at annotations.
|
||||
|
@ -51,16 +51,4 @@ type Provider interface {
|
|||
// token through the provider's STS service.
|
||||
NewTokenForServiceAccount(ctx context.Context, oidcToken string,
|
||||
serviceAccount corev1.ServiceAccount, opts ...Option) (Token, error)
|
||||
|
||||
// ParseArtifactRepository parses the artifact repository to verify if it
|
||||
// is a valid repository for the provider. As a result, it returns the
|
||||
// input required for the provider to issue the registry credentials. This
|
||||
// input is also included as part of the cache key for the issued credentials.
|
||||
ParseArtifactRepository(artifactRepository string) (string, error)
|
||||
|
||||
// NewArtifactRegistryCredentials takes the registry input extracted by
|
||||
// ParseArtifactRepository() and an access token and returns credentials
|
||||
// that can be used to authenticate with the registry.
|
||||
NewArtifactRegistryCredentials(ctx context.Context, registryInput string,
|
||||
accessToken Token, opts ...Option) (*ArtifactRegistryCredentials, error)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
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 auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
)
|
||||
|
||||
type mockProvider struct {
|
||||
t *testing.T
|
||||
|
||||
returnName string
|
||||
returnIdentity string
|
||||
returnIdentityErr string
|
||||
returnRegistryErr string
|
||||
returnRegistryInput string
|
||||
returnRESTConfig *auth.RESTConfig
|
||||
returnRESTConfigOptsErr string
|
||||
returnControllerToken auth.Token
|
||||
returnAccessToken auth.Token
|
||||
returnRegistryOptions []auth.Option
|
||||
returnRegistryToken *auth.ArtifactRegistryCredentials
|
||||
paramAudiences []string
|
||||
paramServiceAccount corev1.ServiceAccount
|
||||
paramOIDCTokenClient *http.Client
|
||||
paramArtifactRepository string
|
||||
paramCluster string
|
||||
paramClusterAddress string
|
||||
paramAccessToken auth.Token
|
||||
paramAccessTokens []auth.Token
|
||||
paramAllowShellOut bool
|
||||
|
||||
// For multi-token flow (RESTConfig)
|
||||
paramFirstScopes []string
|
||||
paramSecondScopes []string
|
||||
expectFirstScopes bool
|
||||
expectSecondScopes bool
|
||||
}
|
||||
|
||||
func (m *mockProvider) GetName() string {
|
||||
return m.returnName
|
||||
}
|
||||
|
||||
func (m *mockProvider) NewControllerToken(ctx context.Context, opts ...auth.Option) (auth.Token, error) {
|
||||
m.checkOptions(opts...)
|
||||
return m.returnControllerToken, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) GetAudiences(ctx context.Context, serviceAccount corev1.ServiceAccount) ([]string, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(serviceAccount).To(Equal(m.paramServiceAccount))
|
||||
return []string{"mock-audience"}, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) GetIdentity(serviceAccount corev1.ServiceAccount) (string, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(serviceAccount).To(Equal(m.paramServiceAccount))
|
||||
if m.returnIdentityErr != "" {
|
||||
return "", errors.New(m.returnIdentityErr)
|
||||
}
|
||||
return m.returnIdentity, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) NewTokenForServiceAccount(ctx context.Context, oidcToken string,
|
||||
serviceAccount corev1.ServiceAccount, opts ...auth.Option) (auth.Token, error) {
|
||||
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
|
||||
// Verify the OIDC token.
|
||||
token, _, err := jwt.NewParser().ParseUnverified(oidcToken, jwt.MapClaims{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
iss, err := token.Claims.GetIssuer()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
ctx = oidc.ClientContext(ctx, m.paramOIDCTokenClient)
|
||||
jwks := oidc.NewRemoteKeySet(ctx, iss+"openid/v1/jwks")
|
||||
clientIDs := m.paramAudiences
|
||||
if len(clientIDs) == 0 {
|
||||
clientIDs = []string{"mock-audience"}
|
||||
}
|
||||
for _, aud := range clientIDs {
|
||||
_, err = oidc.NewVerifier(iss, jwks, &oidc.Config{
|
||||
ClientID: aud,
|
||||
SupportedSigningAlgs: []string{token.Method.Alg()},
|
||||
}).Verify(ctx, oidcToken)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
|
||||
g.Expect(serviceAccount).To(Equal(m.paramServiceAccount))
|
||||
|
||||
m.checkOptions(opts...)
|
||||
|
||||
return m.returnAccessToken, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) ParseArtifactRepository(artifactRepository string) (string, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(artifactRepository).To(Equal(m.paramArtifactRepository))
|
||||
if m.returnRegistryErr != "" {
|
||||
return "", errors.New(m.returnRegistryErr)
|
||||
}
|
||||
return m.returnRegistryInput, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) GetAccessTokenOptionsForArtifactRepository(artifactRepository string) ([]auth.Option, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(artifactRepository).To(Equal(m.paramArtifactRepository))
|
||||
return m.returnRegistryOptions, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) NewArtifactRegistryCredentials(ctx context.Context, registryInput string,
|
||||
accessToken auth.Token, opts ...auth.Option) (*auth.ArtifactRegistryCredentials, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
g.Expect(registryInput).To(Equal(m.paramArtifactRepository))
|
||||
g.Expect(accessToken).To(Equal(m.paramAccessToken))
|
||||
m.checkOptions(opts...)
|
||||
return m.returnRegistryToken, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) GetAccessTokenOptionsForCluster(opts ...auth.Option) ([][]auth.Option, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
g.Expect(o.ClusterResource).To(Equal(m.paramCluster))
|
||||
if m.returnRESTConfigOptsErr != "" {
|
||||
return nil, errors.New(m.returnRESTConfigOptsErr)
|
||||
}
|
||||
return [][]auth.Option{{auth.WithScopes("first-token")}, {auth.WithScopes("second-token")}}, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) NewRESTConfig(ctx context.Context, accessTokens []auth.Token,
|
||||
opts ...auth.Option) (*auth.RESTConfig, error) {
|
||||
m.t.Helper()
|
||||
g := NewWithT(m.t)
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
g.Expect(o.ClusterResource).To(Equal(m.paramCluster))
|
||||
g.Expect(o.ClusterAddress).To(Equal(m.paramClusterAddress))
|
||||
g.Expect(accessTokens).To(Equal(m.paramAccessTokens))
|
||||
m.checkOptions(opts...)
|
||||
return m.returnRESTConfig, nil
|
||||
}
|
||||
|
||||
func (m *mockProvider) checkOptions(opts ...auth.Option) {
|
||||
m.t.Helper()
|
||||
|
||||
g := NewWithT(m.t)
|
||||
|
||||
var o auth.Options
|
||||
o.Apply(opts...)
|
||||
|
||||
// Determine which scopes to expect based on multi-token flow
|
||||
expectedScopes := []string{"scope1", "scope2"}
|
||||
if m.paramFirstScopes != nil && m.paramSecondScopes != nil {
|
||||
switch {
|
||||
case m.expectFirstScopes:
|
||||
expectedScopes = m.paramFirstScopes
|
||||
m.expectFirstScopes = false
|
||||
case m.expectSecondScopes:
|
||||
expectedScopes = m.paramSecondScopes
|
||||
m.expectSecondScopes = false
|
||||
default:
|
||||
expectedScopes = []string{"scope1", "scope2"}
|
||||
}
|
||||
}
|
||||
|
||||
expectedAudiences := []string{"audience1", "audience2"}
|
||||
if m.paramAudiences != nil {
|
||||
expectedAudiences = m.paramAudiences
|
||||
}
|
||||
g.Expect(o.Audiences).To(ConsistOf(expectedAudiences))
|
||||
g.Expect(o.Scopes).To(Equal(expectedScopes))
|
||||
g.Expect(o.STSRegion).To(Equal("us-east-1"))
|
||||
g.Expect(o.STSEndpoint).To(Equal("https://sts.some-cloud.io"))
|
||||
g.Expect(o.ProxyURL).To(Equal(&url.URL{Scheme: "http", Host: "proxy.io:8080"}))
|
||||
g.Expect(o.CAData).To(Equal("ca-data"))
|
||||
g.Expect(o.AllowShellOut).To(Equal(m.paramAllowShellOut))
|
||||
}
|
108
auth/registry.go
108
auth/registry.go
|
@ -17,22 +17,49 @@ limitations under the License.
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/fluxcd/pkg/cache"
|
||||
)
|
||||
|
||||
// ArtifactRegistryCredentialsProvider is an interface that defines methods
|
||||
// for retrieving credentials for artifact registries from cloud providers.
|
||||
type ArtifactRegistryCredentialsProvider interface {
|
||||
Provider
|
||||
|
||||
// GetAccessTokenOptionsForArtifactRepository returns the options that must be
|
||||
// passed to the provider to retrieve access tokens for an artifact repository.
|
||||
GetAccessTokenOptionsForArtifactRepository(artifactRepository string) ([]Option, error)
|
||||
|
||||
// ParseArtifactRepository parses the artifact repository to verify
|
||||
// it's a valid repository for the provider. As a result, it returns
|
||||
// the input required for the provider to issue registry credentials.
|
||||
// This input is included in the cache key for the issued credentials.
|
||||
ParseArtifactRepository(artifactRepository string) (string, error)
|
||||
|
||||
// NewArtifactRegistryCredentials takes the registry input extracted by
|
||||
// ParseArtifactRepository() and an access token and returns credentials
|
||||
// that can be used to authenticate with the registry.
|
||||
NewArtifactRegistryCredentials(ctx context.Context, registryInput string,
|
||||
accessToken Token, opts ...Option) (*ArtifactRegistryCredentials, error)
|
||||
}
|
||||
|
||||
// ArtifactRegistryCredentials is a particular type implementing the Token interface
|
||||
// for credentials that can be used to authenticate against an artifact registry
|
||||
// from a cloud provider. This type is compatible with all the cloud providers
|
||||
// and should be returned when the artifact repository is configured in the options.
|
||||
// from a cloud provider.
|
||||
type ArtifactRegistryCredentials struct {
|
||||
authn.Authenticator
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// GetDuration implements Token.
|
||||
func (a *ArtifactRegistryCredentials) GetDuration() time.Duration {
|
||||
return time.Until(a.ExpiresAt)
|
||||
}
|
||||
|
@ -43,9 +70,84 @@ func GetRegistryFromArtifactRepository(artifactRepository string) (string, error
|
|||
if strings.ContainsRune(registry, '/') {
|
||||
ref, err := name.ParseReference(registry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("failed to parse artifact repository '%s': %w",
|
||||
artifactRepository, err)
|
||||
}
|
||||
return ref.Context().RegistryStr(), nil
|
||||
}
|
||||
return registry, nil
|
||||
}
|
||||
|
||||
// GetArtifactRegistryCredentials retrieves the registry credentials for the
|
||||
// specified artifact repository and provider.
|
||||
func GetArtifactRegistryCredentials(ctx context.Context, provider ArtifactRegistryCredentialsProvider,
|
||||
artifactRepository string, opts ...Option) (*ArtifactRegistryCredentials, error) {
|
||||
|
||||
registryInput, err := provider.ParseArtifactRepository(artifactRepository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// First, we need an access token. This cannot be retrieved inside the
|
||||
// cache lock, otherwise we reach a deadlock.
|
||||
accessTokenOpts, err := provider.GetAccessTokenOptionsForArtifactRepository(artifactRepository)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessTokenOpts = append(opts, accessTokenOpts...)
|
||||
accessToken, err := GetAccessToken(ctx, provider, accessTokenOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access token for artifact registry: %w", err)
|
||||
}
|
||||
|
||||
// Prepare a function to create new credentials.
|
||||
newArtifactRegistryCredentials := func() (*ArtifactRegistryCredentials, error) {
|
||||
creds, err := provider.NewArtifactRegistryCredentials(ctx, registryInput, accessToken, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create artifact registry credentials: %w", err)
|
||||
}
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
var o Options
|
||||
o.Apply(opts...)
|
||||
|
||||
// Bail out early if cache is disabled.
|
||||
if o.Cache == nil {
|
||||
return newArtifactRegistryCredentials()
|
||||
}
|
||||
|
||||
// Build cache key.
|
||||
var serviceAccount *corev1.ServiceAccount
|
||||
var providerIdentity string
|
||||
var audiences []string
|
||||
if o.ServiceAccount != nil {
|
||||
var err error
|
||||
serviceAccount, audiences, providerIdentity, err =
|
||||
getServiceAccountAndProviderInfo(ctx, provider, o.Client, *o.ServiceAccount, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
accessTokenCacheKey := buildAccessTokenCacheKey(provider, audiences,
|
||||
providerIdentity, serviceAccount, accessTokenOpts...)
|
||||
cacheKey := buildCacheKey(
|
||||
fmt.Sprintf("accessTokenCacheKey=%s", accessTokenCacheKey),
|
||||
fmt.Sprintf("artifactRepositoryCacheKey=%s", registryInput))
|
||||
|
||||
// Build involved object details.
|
||||
kind := o.InvolvedObject.Kind
|
||||
name := o.InvolvedObject.Name
|
||||
namespace := o.InvolvedObject.Namespace
|
||||
operation := o.InvolvedObject.Operation
|
||||
|
||||
// Get credentials from cache.
|
||||
creds, _, err := o.Cache.GetOrSet(ctx, cacheKey, func(ctx context.Context) (cache.Token, error) {
|
||||
return newArtifactRegistryCredentials()
|
||||
}, cache.WithInvolvedObject(kind, name, namespace, operation))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return creds.(*ArtifactRegistryCredentials), nil
|
||||
}
|
||||
|
|
|
@ -17,11 +17,19 @@ limitations under the License.
|
|||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
"github.com/fluxcd/pkg/cache"
|
||||
)
|
||||
|
||||
func TestGetRegistryFromArtifactRepository(t *testing.T) {
|
||||
|
@ -86,3 +94,190 @@ func TestGetRegistryFromArtifactRepository(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetArtifactRegistryCredentials(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
kubeClient, oidcClient := newTestEnv(t, ctx)
|
||||
|
||||
// Create a default service account.
|
||||
defaultServiceAccount := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
err := kubeClient.Create(ctx, defaultServiceAccount)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
saRef := client.ObjectKey{
|
||||
Name: defaultServiceAccount.Name,
|
||||
Namespace: defaultServiceAccount.Namespace,
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
provider *mockProvider
|
||||
artifactRepository string
|
||||
opts []auth.Option
|
||||
disableObjectLevel bool
|
||||
expectedCreds *auth.ArtifactRegistryCredentials
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "registry token from controller access token",
|
||||
provider: &mockProvider{
|
||||
returnRegistryInput: "some-registry.io/some/artifact",
|
||||
returnControllerToken: &mockToken{token: "mock-default-token"},
|
||||
returnRegistryToken: &auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}),
|
||||
},
|
||||
paramAccessToken: &mockToken{token: "mock-default-token"},
|
||||
paramArtifactRepository: "some-registry.io/some/artifact",
|
||||
},
|
||||
artifactRepository: "some-registry.io/some/artifact",
|
||||
opts: []auth.Option{
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
},
|
||||
expectedCreds: &auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "registry token from access token from service account",
|
||||
provider: &mockProvider{
|
||||
returnName: "mock-provider",
|
||||
returnRegistryInput: "some-registry.io/some/artifact",
|
||||
returnAccessToken: &mockToken{token: "mock-access-token"},
|
||||
returnRegistryToken: &auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}),
|
||||
},
|
||||
paramAudiences: []string{"audience1", "audience2"},
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
paramOIDCTokenClient: oidcClient,
|
||||
paramArtifactRepository: "some-registry.io/some/artifact",
|
||||
paramAccessToken: &mockToken{token: "mock-access-token"},
|
||||
},
|
||||
artifactRepository: "some-registry.io/some/artifact",
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
},
|
||||
expectedCreds: &auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{Username: "mock-registry-token"}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all the options are taken into account in the cache key",
|
||||
provider: &mockProvider{
|
||||
returnName: "mock-provider",
|
||||
returnIdentity: "mock-identity",
|
||||
returnRegistryInput: "artifact-cache-key",
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
paramArtifactRepository: "some-registry.io/some/artifact",
|
||||
},
|
||||
artifactRepository: "some-registry.io/some/artifact",
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
func(o *auth.Options) {
|
||||
tokenCache, err := cache.NewTokenCache(2)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
const accessTokenKey = "db625bd5a96dc48fcc100659c6db98857d1e0ceec930bbded0fdece14af4307c"
|
||||
var token auth.Token = &mockToken{token: "cached-token"}
|
||||
cachedToken, ok, err := tokenCache.GetOrSet(ctx, accessTokenKey, func(ctx context.Context) (cache.Token, error) {
|
||||
return token, nil
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(ok).To(BeFalse())
|
||||
g.Expect(cachedToken).To(Equal(token))
|
||||
|
||||
const artifactRegistryCredentialsKey = "61fe71ebbf306060d67acbdc2389d5fd816bee40e7685afe2fdc18b7d3bde1d6"
|
||||
token = &auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{Username: "cached-registry-token"}),
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
}
|
||||
cachedToken, ok, err = tokenCache.GetOrSet(ctx, artifactRegistryCredentialsKey, func(ctx context.Context) (cache.Token, error) {
|
||||
return token, nil
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(ok).To(BeFalse())
|
||||
g.Expect(cachedToken).To(Equal(token))
|
||||
|
||||
o.Cache = tokenCache
|
||||
},
|
||||
},
|
||||
expectedCreds: &auth.ArtifactRegistryCredentials{
|
||||
Authenticator: authn.FromConfig(authn.AuthConfig{Username: "cached-registry-token"}),
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error parsing artifact repository",
|
||||
provider: &mockProvider{
|
||||
paramArtifactRepository: "some-registry.io/some/artifact",
|
||||
returnRegistryErr: "mock error",
|
||||
},
|
||||
artifactRepository: "some-registry.io/some/artifact",
|
||||
expectedErr: "mock error",
|
||||
},
|
||||
{
|
||||
name: "disable object level workload identity",
|
||||
provider: &mockProvider{
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
},
|
||||
disableObjectLevel: true,
|
||||
expectedErr: "ObjectLevelWorkloadIdentity feature gate is not enabled",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tt.provider.t = t
|
||||
|
||||
if !tt.disableObjectLevel {
|
||||
t.Setenv(auth.EnvVarEnableObjectLevelWorkloadIdentity, "true")
|
||||
}
|
||||
|
||||
creds, err := auth.GetArtifactRegistryCredentials(ctx, tt.provider, tt.artifactRepository, tt.opts...)
|
||||
|
||||
if tt.expectedErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
|
||||
g.Expect(creds).To(BeNil())
|
||||
} else {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(creds).To(Equal(tt.expectedCreds))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/fluxcd/pkg/cache"
|
||||
)
|
||||
|
||||
// RESTConfigProvider is an interface that defines methods for retrieving
|
||||
// REST configurations for Kubernetes clusters from cloud providers.
|
||||
type RESTConfigProvider interface {
|
||||
Provider
|
||||
|
||||
// GetAccessTokenOptionsForCluster returns the options that must be
|
||||
// passed to the provider to retrieve access tokens for a cluster.
|
||||
// More than one access token may be required depending on the
|
||||
// provider, with different options (e.g. scope). Hence the return
|
||||
// type is a slice of []Option.
|
||||
GetAccessTokenOptionsForCluster(opts ...Option) ([][]Option, error)
|
||||
|
||||
// NewRESTConfig returns a new RESTConfig that can be used to authenticate
|
||||
// with the Kubernetes API server. The access tokens are used for looking
|
||||
// up connection details like the API server address and CA certificate
|
||||
// data, and for accessing the cluster API server itself via the IAM
|
||||
// system of the cloud provider. If it's just a single token or multiple,
|
||||
// it depends on the provider.
|
||||
NewRESTConfig(ctx context.Context, accessTokens []Token, opts ...Option) (*RESTConfig, error)
|
||||
}
|
||||
|
||||
// RESTConfig is a particular type implementing the Token interface
|
||||
// for Kubernetes REST configurations.
|
||||
type RESTConfig struct {
|
||||
Host string
|
||||
BearerToken string
|
||||
CAData []byte
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// GetDuration implements Token.
|
||||
func (r *RESTConfig) GetDuration() time.Duration {
|
||||
return time.Until(r.ExpiresAt)
|
||||
}
|
||||
|
||||
// ParseClusterAddress parses the given cluster address and returns
|
||||
// the canonical form https://<lowercase(host)>:<port>.
|
||||
func ParseClusterAddress(address string) (string, error) {
|
||||
if address == "" {
|
||||
return "", errors.New("empty address")
|
||||
}
|
||||
if !strings.HasPrefix(address, "http") {
|
||||
address = fmt.Sprintf("https://%s", address)
|
||||
}
|
||||
u, err := url.Parse(address)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse Kubernetes API server address '%s': %w", address, err)
|
||||
}
|
||||
if u.Scheme != "https" {
|
||||
return "", fmt.Errorf("the Kubernetes API server address '%s' must use https scheme", address)
|
||||
}
|
||||
host := u.Host
|
||||
if u.Port() == "" {
|
||||
host += ":443"
|
||||
}
|
||||
return fmt.Sprintf("https://%s", strings.ToLower(host)), nil
|
||||
}
|
||||
|
||||
// GetRESTConfig retrieves the authentication and connection
|
||||
// details to a remote Kubernetes cluster for the given provider,
|
||||
// cluster resource name and options.
|
||||
func GetRESTConfig(ctx context.Context, provider RESTConfigProvider, opts ...Option) (*RESTConfig, error) {
|
||||
|
||||
var o Options
|
||||
o.Apply(opts...)
|
||||
|
||||
// First, we need the access tokens. They cannot be retrieved inside the
|
||||
// cache lock, otherwise we reach a deadlock.
|
||||
accessTokenOpts, err := provider.GetAccessTokenOptionsForCluster(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessTokens := make([]Token, 0, len(accessTokenOpts))
|
||||
for i := range accessTokenOpts {
|
||||
accessTokenOpts[i] = append(slices.Clone(opts), accessTokenOpts[i]...)
|
||||
token, err := GetAccessToken(ctx, provider, accessTokenOpts[i]...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access token for cluster: %w", err)
|
||||
}
|
||||
accessTokens = append(accessTokens, token)
|
||||
}
|
||||
|
||||
// Prepare a function to create the restconfig if needed.
|
||||
newRESTConfig := func() (*RESTConfig, error) {
|
||||
conf, err := provider.NewRESTConfig(ctx, accessTokens, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// Bail out early if cache is disabled.
|
||||
if o.Cache == nil {
|
||||
return newRESTConfig()
|
||||
}
|
||||
|
||||
// Build cache key.
|
||||
var serviceAccount *corev1.ServiceAccount
|
||||
var providerIdentity string
|
||||
var audiences []string
|
||||
if o.ServiceAccount != nil {
|
||||
var err error
|
||||
serviceAccount, audiences, providerIdentity, err =
|
||||
getServiceAccountAndProviderInfo(ctx, provider, o.Client, *o.ServiceAccount, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var cacheKeyParts []string
|
||||
for i, atOpts := range accessTokenOpts {
|
||||
key := buildAccessTokenCacheKey(provider, audiences, providerIdentity, serviceAccount, atOpts...)
|
||||
cacheKeyParts = append(cacheKeyParts, fmt.Sprintf("accessToken%dCacheKey=%s", i, key))
|
||||
}
|
||||
if c := o.ClusterResource; c != "" {
|
||||
cacheKeyParts = append(cacheKeyParts, fmt.Sprintf("cluster=%s", c))
|
||||
}
|
||||
if a := o.ClusterAddress; a != "" {
|
||||
cacheKeyParts = append(cacheKeyParts, fmt.Sprintf("address=%s", a))
|
||||
}
|
||||
cacheKey := buildCacheKey(cacheKeyParts...)
|
||||
|
||||
// Build involved object details.
|
||||
cacheOpts := []cache.Options{cache.WithInvolvedObject(
|
||||
o.InvolvedObject.Kind,
|
||||
o.InvolvedObject.Name,
|
||||
o.InvolvedObject.Namespace,
|
||||
o.InvolvedObject.Operation)}
|
||||
|
||||
// Get restconfig from cache.
|
||||
token, _, err := o.Cache.GetOrSet(ctx, cacheKey, func(ctx context.Context) (cache.Token, error) {
|
||||
return newRESTConfig()
|
||||
}, cacheOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token.(*RESTConfig), nil
|
||||
}
|
|
@ -0,0 +1,322 @@
|
|||
/*
|
||||
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 auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
"github.com/fluxcd/pkg/cache"
|
||||
)
|
||||
|
||||
func TestParseClusterAddress(t *testing.T) {
|
||||
tests := []struct {
|
||||
address string
|
||||
expected string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
address: "https://example.com:443",
|
||||
expected: "https://example.com:443",
|
||||
},
|
||||
{
|
||||
address: "example.com",
|
||||
expected: "https://example.com:443",
|
||||
},
|
||||
{
|
||||
address: "EXAMPLE.COM:8080",
|
||||
expected: "https://example.com:8080",
|
||||
},
|
||||
{
|
||||
address: "34.44.60.80",
|
||||
expected: "https://34.44.60.80:443",
|
||||
},
|
||||
{
|
||||
address: "",
|
||||
err: "empty address",
|
||||
},
|
||||
{
|
||||
address: "------------\t",
|
||||
err: "failed to parse Kubernetes API server address 'https://------------ ':",
|
||||
},
|
||||
{
|
||||
address: "http://example.com:443",
|
||||
err: "Kubernetes API server address 'http://example.com:443' must use https scheme",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(strings.ReplaceAll(tt.address, "/", ""), func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
address, err := auth.ParseClusterAddress(tt.address)
|
||||
|
||||
if tt.err != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.err))
|
||||
g.Expect(address).To(BeEmpty())
|
||||
} else {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(address).To(Equal(tt.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRESTConfig(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
kubeClient, oidcClient := newTestEnv(t, ctx)
|
||||
|
||||
// Create a default service account.
|
||||
defaultServiceAccount := &corev1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
err := kubeClient.Create(ctx, defaultServiceAccount)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
saRef := client.ObjectKey{
|
||||
Name: defaultServiceAccount.Name,
|
||||
Namespace: defaultServiceAccount.Namespace,
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
provider *mockProvider
|
||||
cluster string
|
||||
opts []auth.Option
|
||||
disableObjectLevel bool
|
||||
expectedCreds *auth.RESTConfig
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "restconfig from controller access token",
|
||||
provider: &mockProvider{
|
||||
returnControllerToken: &mockToken{token: "mock-default-token"},
|
||||
returnRESTConfig: &auth.RESTConfig{
|
||||
Host: "https://cluster/resource/name",
|
||||
BearerToken: "mock-bearer-token",
|
||||
CAData: []byte("ca-data"),
|
||||
},
|
||||
paramCluster: "cluster/resource/name",
|
||||
paramFirstScopes: []string{"first-token"},
|
||||
paramSecondScopes: []string{"second-token"},
|
||||
expectFirstScopes: true,
|
||||
expectSecondScopes: true,
|
||||
paramAccessTokens: []auth.Token{
|
||||
&mockToken{token: "mock-default-token"},
|
||||
&mockToken{token: "mock-default-token"},
|
||||
},
|
||||
},
|
||||
cluster: "cluster/resource/name",
|
||||
opts: []auth.Option{
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
},
|
||||
expectedCreds: &auth.RESTConfig{
|
||||
Host: "https://cluster/resource/name",
|
||||
BearerToken: "mock-bearer-token",
|
||||
CAData: []byte("ca-data"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "restconfig from access token from service account",
|
||||
provider: &mockProvider{
|
||||
returnName: "mock-provider",
|
||||
returnAccessToken: &mockToken{token: "mock-access-token"},
|
||||
returnRESTConfig: &auth.RESTConfig{
|
||||
Host: "https://cluster/resource/name",
|
||||
BearerToken: "mock-bearer-token",
|
||||
CAData: []byte("ca-data"),
|
||||
},
|
||||
paramAudiences: []string{"audience1", "audience2"},
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
paramOIDCTokenClient: oidcClient,
|
||||
paramCluster: "cluster/resource/name",
|
||||
paramFirstScopes: []string{"first-token"},
|
||||
paramSecondScopes: []string{"second-token"},
|
||||
expectFirstScopes: true,
|
||||
expectSecondScopes: true,
|
||||
paramAccessTokens: []auth.Token{
|
||||
&mockToken{token: "mock-access-token"},
|
||||
&mockToken{token: "mock-access-token"},
|
||||
},
|
||||
},
|
||||
cluster: "cluster/resource/name",
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
},
|
||||
expectedCreds: &auth.RESTConfig{
|
||||
Host: "https://cluster/resource/name",
|
||||
BearerToken: "mock-bearer-token",
|
||||
CAData: []byte("ca-data"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all the options are taken into account in the cache key",
|
||||
provider: &mockProvider{
|
||||
returnName: "mock-provider",
|
||||
returnIdentity: "mock-identity",
|
||||
paramAudiences: []string{"audience1", "audience2"},
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
paramCluster: "cluster/resource/name",
|
||||
paramClusterAddress: "https://cluster/resource/name",
|
||||
paramFirstScopes: []string{"first-token"},
|
||||
paramSecondScopes: []string{"second-token"},
|
||||
expectFirstScopes: true,
|
||||
expectSecondScopes: true,
|
||||
},
|
||||
cluster: "cluster/resource/name",
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
auth.WithClusterResource("cluster/resource/name"),
|
||||
auth.WithClusterAddress("https://cluster/resource/name"),
|
||||
func(o *auth.Options) {
|
||||
tokenCache, err := cache.NewTokenCache(3)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
accessTokenKey := "500a3116f5d1c492d7a5ea97cdf9a7f869815346c79f01c7368703c241ebb5eb"
|
||||
var token auth.Token = &mockToken{token: "cached-token"}
|
||||
cachedToken, ok, err := tokenCache.GetOrSet(ctx, accessTokenKey, func(ctx context.Context) (cache.Token, error) {
|
||||
return token, nil
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(ok).To(BeFalse())
|
||||
g.Expect(cachedToken).To(Equal(token))
|
||||
|
||||
accessTokenKey = "0b1167fc851943c6153d40e149cd2970aac121aaf03b1fcad158672974f58827"
|
||||
token = &mockToken{token: "cached-token"}
|
||||
cachedToken, ok, err = tokenCache.GetOrSet(ctx, accessTokenKey, func(ctx context.Context) (cache.Token, error) {
|
||||
return token, nil
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(ok).To(BeFalse())
|
||||
g.Expect(cachedToken).To(Equal(token))
|
||||
|
||||
const restConfigKey = "a1937b7b1df13ac8ad784db686088c4cd5b4c4877318d07d3fa19ab8caf9d7c2"
|
||||
token = &auth.RESTConfig{
|
||||
Host: "https://cluster/resource/name",
|
||||
BearerToken: "mock-bearer-token",
|
||||
CAData: []byte("ca-data"),
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
}
|
||||
cachedToken, ok, err = tokenCache.GetOrSet(ctx, restConfigKey, func(ctx context.Context) (cache.Token, error) {
|
||||
return token, nil
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(ok).To(BeFalse())
|
||||
g.Expect(cachedToken).To(Equal(token))
|
||||
|
||||
o.Cache = tokenCache
|
||||
},
|
||||
},
|
||||
expectedCreds: &auth.RESTConfig{
|
||||
Host: "https://cluster/resource/name",
|
||||
BearerToken: "mock-bearer-token",
|
||||
CAData: []byte("ca-data"),
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error getting access token options for cluster",
|
||||
provider: &mockProvider{
|
||||
paramCluster: "cluster/resource/name",
|
||||
returnRESTConfigOptsErr: "mock error",
|
||||
},
|
||||
cluster: "cluster/resource/name",
|
||||
expectedErr: "mock error",
|
||||
},
|
||||
{
|
||||
name: "disable object level workload identity",
|
||||
provider: &mockProvider{
|
||||
paramServiceAccount: *defaultServiceAccount,
|
||||
paramFirstScopes: []string{"first-token"},
|
||||
paramSecondScopes: []string{"second-token"},
|
||||
expectFirstScopes: true,
|
||||
expectSecondScopes: true,
|
||||
},
|
||||
opts: []auth.Option{
|
||||
auth.WithServiceAccount(saRef, kubeClient),
|
||||
auth.WithAudiences("audience1", "audience2"),
|
||||
auth.WithScopes("scope1", "scope2"),
|
||||
auth.WithSTSRegion("us-east-1"),
|
||||
auth.WithSTSEndpoint("https://sts.some-cloud.io"),
|
||||
auth.WithProxyURL(url.URL{Scheme: "http", Host: "proxy.io:8080"}),
|
||||
auth.WithCAData("ca-data"),
|
||||
},
|
||||
disableObjectLevel: true,
|
||||
expectedErr: "ObjectLevelWorkloadIdentity feature gate is not enabled",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tt.provider.t = t
|
||||
|
||||
if !tt.disableObjectLevel {
|
||||
t.Setenv(auth.EnvVarEnableObjectLevelWorkloadIdentity, "true")
|
||||
}
|
||||
|
||||
if tt.cluster != "" {
|
||||
tt.opts = append(tt.opts, auth.WithClusterResource(tt.cluster))
|
||||
}
|
||||
|
||||
creds, err := auth.GetRESTConfig(ctx, tt.provider, tt.opts...)
|
||||
|
||||
if tt.expectedErr != "" {
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.expectedErr))
|
||||
g.Expect(creds).To(BeNil())
|
||||
} else {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(creds).To(Equal(tt.expectedCreds))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
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 auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
)
|
||||
|
||||
func newTestEnv(t *testing.T, ctx context.Context) (client.Client, *http.Client) {
|
||||
t.Helper()
|
||||
g := NewWithT(t)
|
||||
|
||||
// Create test env.
|
||||
testEnv := &envtest.Environment{}
|
||||
conf, err := testEnv.Start()
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
t.Cleanup(func() { testEnv.Stop() })
|
||||
kubeClient, err := client.New(conf, client.Options{})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Create HTTP client for OIDC verification.
|
||||
clusterCAPool := x509.NewCertPool()
|
||||
ok := clusterCAPool.AppendCertsFromPEM(conf.TLSClientConfig.CAData)
|
||||
g.Expect(ok).To(BeTrue())
|
||||
oidcClient := &http.Client{}
|
||||
oidcClient.Transport = http.DefaultTransport.(*http.Transport).Clone()
|
||||
oidcClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||
RootCAs: clusterCAPool,
|
||||
}
|
||||
|
||||
// Grant anonymous access to service account issuer discovery.
|
||||
err = kubeClient.Create(ctx, &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "anonymous-service-account-issuer-discovery",
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "User",
|
||||
Name: "system:anonymous",
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: "system:service-account-issuer-discovery",
|
||||
},
|
||||
})
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
return kubeClient, oidcClient
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
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 auth_test
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type mockToken struct {
|
||||
token string
|
||||
}
|
||||
|
||||
func (m *mockToken) GetDuration() time.Duration {
|
||||
return time.Hour
|
||||
}
|
|
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
// authutils contains small utility functions without much logic
|
||||
// Package utils contains small utility functions without much logic
|
||||
// wrapping the major APIs of the core auth package for ease of use
|
||||
// in the controllers. These functions also import the provider
|
||||
// packages to wrap switch-case choice of provider implementations.
|
||||
// Because of that, these functions cannot be placed in the core
|
||||
// package as they would cause a cyclic dependency given that the
|
||||
// provider packages also import the core package.
|
||||
package authutils
|
||||
package utils
|
||||
|
|
|
@ -14,10 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authutils
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
"github.com/fluxcd/pkg/auth/azure"
|
||||
|
@ -37,7 +38,7 @@ func GetGitCredentials(ctx context.Context, providerName string, opts ...auth.Op
|
|||
switch providerName {
|
||||
case azure.ProviderName:
|
||||
opts = append(opts, auth.WithScopes(azure.ScopeDevOps))
|
||||
token, err := auth.GetToken(ctx, azure.Provider{}, opts...)
|
||||
token, err := auth.GetAccessToken(ctx, azure.Provider{}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -45,6 +46,6 @@ func GetGitCredentials(ctx context.Context, providerName string, opts ...auth.Op
|
|||
BearerToken: token.(*azure.Token).Token,
|
||||
}, nil
|
||||
default:
|
||||
return nil, ErrUnsupportedProvider
|
||||
return nil, fmt.Errorf("provider '%s' does not support Git credentials", providerName)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
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 utils_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
authutils "github.com/fluxcd/pkg/auth/utils"
|
||||
)
|
||||
|
||||
func TestGetGitCredentials(t *testing.T) {
|
||||
t.Run("azure", func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
p, err := authutils.GetGitCredentials(ctx, "azure")
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).NotTo(ContainSubstring("does not support Git credentials"))
|
||||
g.Expect(p).To(BeNil())
|
||||
})
|
||||
|
||||
t.Run("unknown provider", func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
p, err := authutils.GetGitCredentials(context.Background(), "unknown")
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(Equal("provider 'unknown' does not support Git credentials"))
|
||||
g.Expect(p).To(BeNil())
|
||||
})
|
||||
}
|
|
@ -14,25 +14,39 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authutils
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
"fmt"
|
||||
|
||||
"github.com/fluxcd/pkg/auth/aws"
|
||||
"github.com/fluxcd/pkg/auth/azure"
|
||||
"github.com/fluxcd/pkg/auth/gcp"
|
||||
"github.com/fluxcd/pkg/auth/generic"
|
||||
)
|
||||
|
||||
// ProviderByName looks up the implemented providers by name.
|
||||
func ProviderByName(name string) auth.Provider {
|
||||
// ProviderByName looks up the implemented providers by name and type.
|
||||
func ProviderByName[T any](name string) (T, error) {
|
||||
var p any
|
||||
var zero T
|
||||
|
||||
switch name {
|
||||
case aws.ProviderName:
|
||||
return aws.Provider{}
|
||||
p = aws.Provider{}
|
||||
case azure.ProviderName:
|
||||
return azure.Provider{}
|
||||
p = azure.Provider{}
|
||||
case gcp.ProviderName:
|
||||
return gcp.Provider{}
|
||||
p = gcp.Provider{}
|
||||
case generic.ProviderName:
|
||||
p = generic.Provider{}
|
||||
default:
|
||||
return nil
|
||||
return zero, fmt.Errorf("provider '%s' not implemented", name)
|
||||
}
|
||||
|
||||
provider, ok := p.(T)
|
||||
if !ok {
|
||||
return zero, fmt.Errorf("provider '%s' does not implement the expected interface", name)
|
||||
}
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
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 utils_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
"github.com/fluxcd/pkg/auth/aws"
|
||||
"github.com/fluxcd/pkg/auth/azure"
|
||||
"github.com/fluxcd/pkg/auth/gcp"
|
||||
"github.com/fluxcd/pkg/auth/generic"
|
||||
authutils "github.com/fluxcd/pkg/auth/utils"
|
||||
)
|
||||
|
||||
func TestProviderByName(t *testing.T) {
|
||||
t.Run("sts providers", func(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
provider any
|
||||
}{
|
||||
{
|
||||
name: azure.ProviderName,
|
||||
provider: azure.Provider{},
|
||||
},
|
||||
{
|
||||
name: aws.ProviderName,
|
||||
provider: aws.Provider{},
|
||||
},
|
||||
{
|
||||
name: gcp.ProviderName,
|
||||
provider: gcp.Provider{},
|
||||
},
|
||||
{
|
||||
name: generic.ProviderName,
|
||||
provider: generic.Provider{},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
p, err := authutils.ProviderByName[auth.Provider](tt.name)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(p).To(Equal(tt.provider))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("registry providers", func(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
provider any
|
||||
}{
|
||||
{
|
||||
name: azure.ProviderName,
|
||||
provider: azure.Provider{},
|
||||
},
|
||||
{
|
||||
name: aws.ProviderName,
|
||||
provider: aws.Provider{},
|
||||
},
|
||||
{
|
||||
name: gcp.ProviderName,
|
||||
provider: gcp.Provider{},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
p, err := authutils.ProviderByName[auth.ArtifactRegistryCredentialsProvider](tt.name)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(p).To(Equal(tt.provider))
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("generic provider", func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
p, err := authutils.ProviderByName[auth.ArtifactRegistryCredentialsProvider](generic.ProviderName)
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring("does not implement the expected interface"))
|
||||
g.Expect(p).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("restconfig providers", func(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
provider any
|
||||
}{
|
||||
{
|
||||
name: azure.ProviderName,
|
||||
provider: azure.Provider{},
|
||||
},
|
||||
{
|
||||
name: aws.ProviderName,
|
||||
provider: aws.Provider{},
|
||||
},
|
||||
{
|
||||
name: gcp.ProviderName,
|
||||
provider: gcp.Provider{},
|
||||
},
|
||||
{
|
||||
name: generic.ProviderName,
|
||||
provider: generic.Provider{},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
p, err := authutils.ProviderByName[auth.RESTConfigProvider](tt.name)
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
g.Expect(p).To(Equal(tt.provider))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("errors", func(t *testing.T) {
|
||||
type iface interface{ foo() }
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
provider any
|
||||
}{
|
||||
{
|
||||
name: azure.ProviderName,
|
||||
provider: azure.Provider{},
|
||||
},
|
||||
{
|
||||
name: aws.ProviderName,
|
||||
provider: aws.Provider{},
|
||||
},
|
||||
{
|
||||
name: gcp.ProviderName,
|
||||
provider: gcp.Provider{},
|
||||
},
|
||||
{
|
||||
name: generic.ProviderName,
|
||||
provider: generic.Provider{},
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
p, err := authutils.ProviderByName[iface](tt.name)
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(ContainSubstring("does not implement the expected interface"))
|
||||
g.Expect(p).To(BeNil())
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("unknown provider", func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
p, err := authutils.ProviderByName[iface]("unknown")
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(err.Error()).To(Equal("provider 'unknown' not implemented"))
|
||||
g.Expect(p).To(BeNil())
|
||||
})
|
||||
})
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package authutils
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -24,29 +24,15 @@ import (
|
|||
"github.com/fluxcd/pkg/auth"
|
||||
)
|
||||
|
||||
// GetArtifactRegistryCredentials retrieves the credentials for the specified
|
||||
// artifact repository using the specified provider. It returns an
|
||||
// authn.Authenticator that can be used to authenticate with the registry.
|
||||
func GetArtifactRegistryCredentials(ctx context.Context,
|
||||
providerName, artifactRepository string,
|
||||
opts ...auth.Option) (authn.Authenticator, error) {
|
||||
// GetArtifactRegistryCredentials retrieves the registry credentials for the
|
||||
// specified artifact repository and provider.
|
||||
func GetArtifactRegistryCredentials(ctx context.Context, providerName string,
|
||||
artifactRepository string, opts ...auth.Option) (authn.Authenticator, error) {
|
||||
|
||||
provider := ProviderByName(providerName)
|
||||
if provider == nil {
|
||||
return nil, ErrUnsupportedProvider
|
||||
}
|
||||
|
||||
opts = append(opts, auth.WithArtifactRepository(artifactRepository))
|
||||
|
||||
token, err := auth.GetToken(ctx, provider, opts...)
|
||||
provider, err := ProviderByName[auth.ArtifactRegistryCredentialsProvider](providerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authenticator, ok := token.(authn.Authenticator)
|
||||
if !ok {
|
||||
return nil, ErrProviderDoesNotSupportRegistry
|
||||
}
|
||||
|
||||
return authenticator, nil
|
||||
return auth.GetArtifactRegistryCredentials(ctx, provider, artifactRepository, opts...)
|
||||
}
|
||||
|
|
|
@ -1,79 +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 authutils_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
authutils "github.com/fluxcd/pkg/auth/utils"
|
||||
)
|
||||
|
||||
// TestGetArtifactRegistryCredentials_ProviderLookup tests the provider lookup
|
||||
// behavior of GetArtifactRegistryCredentials. Full function testing is difficult
|
||||
// because auth.GetToken is not interface-based and would require complex mocking.
|
||||
func TestGetArtifactRegistryCredentials_ProviderLookup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
providerName string
|
||||
expectUnsupportedError bool
|
||||
}{
|
||||
{
|
||||
name: "unsupported provider",
|
||||
providerName: "unsupported-provider",
|
||||
expectUnsupportedError: true,
|
||||
},
|
||||
{
|
||||
name: "AWS provider",
|
||||
providerName: "aws",
|
||||
expectUnsupportedError: false,
|
||||
},
|
||||
{
|
||||
name: "Azure provider",
|
||||
providerName: "azure",
|
||||
expectUnsupportedError: false,
|
||||
},
|
||||
{
|
||||
name: "GCP provider",
|
||||
providerName: "gcp",
|
||||
expectUnsupportedError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
g := NewWithT(t)
|
||||
ctx := context.Background()
|
||||
|
||||
authenticator, err := authutils.GetArtifactRegistryCredentials(ctx, tt.providerName, "registry.example.com")
|
||||
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
g.Expect(authenticator).To(BeNil())
|
||||
|
||||
if tt.expectUnsupportedError {
|
||||
g.Expect(err).To(MatchError(authutils.ErrUnsupportedProvider))
|
||||
} else {
|
||||
g.Expect(err).NotTo(MatchError(authutils.ErrUnsupportedProvider))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
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 utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/fluxcd/pkg/apis/meta"
|
||||
"github.com/fluxcd/pkg/auth"
|
||||
)
|
||||
|
||||
// RESTConfigFetcher is a function that retrieves a *rest.Config for a given
|
||||
// meta.KubeConfigReference, a namespace, and a controller-runtime client.
|
||||
type RESTConfigFetcher func(ctx context.Context, ref meta.KubeConfigReference,
|
||||
namespace string, ctrlClient client.Client) (*rest.Config, error)
|
||||
|
||||
// GetRESTConfigFetcher is a convenience function for controllers that use the
|
||||
// runtime/client.(*Impersonator) to create controller-runtime clients. To keep
|
||||
// runtime decoupled from auth, this function closes over the controller-provided
|
||||
// options and returns a function that can be called by runtime without runtime
|
||||
// needing to know about the type auth.Option. Usage example:
|
||||
//
|
||||
// provider := authutils.GetRESTConfigFetcher(opts...)
|
||||
// impersonatorOpts = append(impersonatorOpts,
|
||||
// runtimeclient.WithKubeConfig(ref, kubeConfOpts, namespace, provider))
|
||||
//
|
||||
// Controllers that don't use the runtime/client.(*Impersonator) can simply call
|
||||
// GetRESTConfig directly, passing the options as variadic arguments.
|
||||
func GetRESTConfigFetcher(opts ...auth.Option) RESTConfigFetcher {
|
||||
return func(ctx context.Context, ref meta.KubeConfigReference,
|
||||
namespace string, ctrlClient client.Client) (*rest.Config, error) {
|
||||
return GetRESTConfig(ctx, ref, namespace, ctrlClient, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// GetRESTConfig retrieves a *rest.Config for the given meta.KubeConfigReference,
|
||||
// namespace, controller-runtime client and options. It's a convenience wrapper
|
||||
// for auth.GetRESTConfig so controllers can pass a meta.KubeConfigReference
|
||||
// object directly without converting it to auth.Option(s).
|
||||
//
|
||||
// Additionally, the resulting *rest.Config will call auth.GetRESTConfig for every
|
||||
// HTTP request to the remote cluster. This is needed for long-running operations
|
||||
// that wait on resources until a potentially long timeout is reached, like kstatus
|
||||
// health checks, and whatever Helm does. The timeout may be longer than a token's
|
||||
// lifetime, so tokens can expire during such operations. auth.GetRESTConfig will
|
||||
// create a fresh token if that happens.
|
||||
//
|
||||
// With the resulting *rest.Config, if a cache is not set in the options, a fresh
|
||||
// token will be created for every HTTP request sent to the remote cluster.
|
||||
func GetRESTConfig(ctx context.Context,
|
||||
kubeConfigRef meta.KubeConfigReference,
|
||||
namespace string, ctrlClient client.Client,
|
||||
opts ...auth.Option) (*rest.Config, error) {
|
||||
|
||||
// Get ConfigMap.
|
||||
cmKey := client.ObjectKey{
|
||||
Name: kubeConfigRef.ConfigMapRef.Name,
|
||||
Namespace: namespace,
|
||||
}
|
||||
var cm corev1.ConfigMap
|
||||
if err := ctrlClient.Get(ctx, cmKey, &cm); err != nil {
|
||||
return nil, fmt.Errorf("failed to get configmap %s: %w", cmKey.String(), err)
|
||||
}
|
||||
|
||||
// Get provider by name.
|
||||
provider, err := ProviderByName[auth.RESTConfigProvider](cm.Data[meta.KubeConfigKeyProvider])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Configure options.
|
||||
if c, ok := cm.Data[meta.KubeConfigKeyCluster]; ok {
|
||||
opts = append(opts, auth.WithClusterResource(c))
|
||||
}
|
||||
if a, ok := cm.Data[meta.KubeConfigKeyAddress]; ok {
|
||||
opts = append(opts, auth.WithClusterAddress(a))
|
||||
}
|
||||
if ca, ok := cm.Data[meta.KubeConfigKeyCACert]; ok {
|
||||
opts = append(opts, auth.WithCAData(ca))
|
||||
}
|
||||
if name, ok := cm.Data[meta.KubeConfigKeyServiceAccountName]; ok {
|
||||
saKey := client.ObjectKey{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
}
|
||||
opts = append(opts, auth.WithServiceAccount(saKey, ctrlClient))
|
||||
} else {
|
||||
opts = append(opts, auth.WithClient(ctrlClient))
|
||||
}
|
||||
if a, ok := cm.Data[meta.KubeConfigKeyAudiences]; ok {
|
||||
var audiences []string
|
||||
for aud := range strings.SplitSeq(a, "\n") {
|
||||
aud = strings.TrimSpace(aud)
|
||||
if aud == "" {
|
||||
continue
|
||||
}
|
||||
audiences = append(audiences, aud)
|
||||
}
|
||||
opts = append(opts, auth.WithAudiences(audiences...))
|
||||
}
|
||||
|
||||
conf, err := auth.GetRESTConfig(ctx, provider, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build wrapped *rest.Config that will call
|
||||
// auth.GetRESTConfig for every HTTP request.
|
||||
restConfig := &rest.Config{
|
||||
Host: conf.Host,
|
||||
TLSClientConfig: rest.TLSClientConfig{CAData: conf.CAData},
|
||||
}
|
||||
restConfig.Wrap(func(base http.RoundTripper) http.RoundTripper {
|
||||
return &restConfigRoundTripper{
|
||||
base: base,
|
||||
provider: provider,
|
||||
opts: opts,
|
||||
}
|
||||
})
|
||||
|
||||
return restConfig, nil
|
||||
}
|
||||
|
||||
// restConfigRoundTripper is an http.RoundTripper that wraps the base
|
||||
// RoundTripper and retrieves a bearer token for the remote cluster
|
||||
// using auth.GetRESTConfig before each HTTP request.
|
||||
type restConfigRoundTripper struct {
|
||||
base http.RoundTripper
|
||||
provider auth.RESTConfigProvider
|
||||
opts []auth.Option
|
||||
}
|
||||
|
||||
// RoundTrip implements http.RoundTripper.
|
||||
func (r *restConfigRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
details, err := auth.GetRESTConfig(req.Context(), r.provider, r.opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+details.BearerToken)
|
||||
return r.base.RoundTrip(req)
|
||||
}
|
|
@ -20,9 +20,9 @@ require (
|
|||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
@ -41,14 +41,14 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
|||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
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/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -19,8 +19,8 @@ package chartutil
|
|||
import (
|
||||
"io"
|
||||
|
||||
goyaml "go.yaml.in/yaml/v2"
|
||||
"sigs.k8s.io/yaml"
|
||||
goyaml "sigs.k8s.io/yaml/goyaml.v2"
|
||||
)
|
||||
|
||||
// PreEncoder allows for pre-processing of the YAML data before encoding.
|
||||
|
|
|
@ -9,15 +9,16 @@ replace github.com/fluxcd/pkg/apis/meta => ../apis/meta
|
|||
replace github.com/opencontainers/go-digest => github.com/opencontainers/go-digest v1.0.1-0.20231025023718-d50d2fec9c98
|
||||
|
||||
require (
|
||||
github.com/fluxcd/pkg/apis/meta v1.12.0
|
||||
github.com/fluxcd/pkg/apis/meta v1.18.0
|
||||
github.com/go-logr/logr v1.4.2
|
||||
github.com/onsi/gomega v1.37.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
helm.sh/helm/v3 v3.18.0
|
||||
k8s.io/api v0.33.0
|
||||
k8s.io/apimachinery v0.33.0
|
||||
go.yaml.in/yaml/v2 v2.4.2
|
||||
helm.sh/helm/v3 v3.18.4
|
||||
k8s.io/api v0.33.2
|
||||
k8s.io/apimachinery v0.33.2
|
||||
sigs.k8s.io/controller-runtime v0.21.0
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
sigs.k8s.io/yaml v1.5.0
|
||||
)
|
||||
|
||||
// Fix CVE-2022-28948
|
||||
|
@ -50,18 +51,18 @@ require (
|
|||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/oauth2 v0.29.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/term v0.33.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.33.0 // indirect
|
||||
k8s.io/client-go v0.33.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.33.2 // indirect
|
||||
k8s.io/client-go v0.33.2 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
|
||||
|
|
|
@ -99,6 +99,10 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
|||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
|
||||
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
|
||||
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=
|
||||
|
@ -108,32 +112,32 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/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/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
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=
|
||||
|
@ -149,16 +153,16 @@ 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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
helm.sh/helm/v3 v3.18.0 h1:ItOAm3Quo0dus3NUHjs+lluqWWEIO7xrSW+zKWCrvlw=
|
||||
helm.sh/helm/v3 v3.18.0/go.mod h1:43QHS1W97RcoFJRk36ZBhHdTfykqBlJdsWp3yhzdq8w=
|
||||
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
|
||||
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
|
||||
k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
|
||||
k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
|
||||
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
||||
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
|
||||
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
|
||||
helm.sh/helm/v3 v3.18.4 h1:pNhnHM3nAmDrxz6/UC+hfjDY4yeDATQCka2/87hkZXQ=
|
||||
helm.sh/helm/v3 v3.18.4/go.mod h1:WVnwKARAw01iEdjpEkP7Ii1tT1pTPYfM1HsakFKM3LI=
|
||||
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
|
||||
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
|
||||
k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8=
|
||||
k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8=
|
||||
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
|
||||
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
|
||||
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
|
||||
|
@ -174,5 +178,6 @@ sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
|||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
|
||||
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
|
||||
|
|
|
@ -19,9 +19,19 @@ package chartutil
|
|||
import (
|
||||
"sort"
|
||||
|
||||
goyaml "sigs.k8s.io/yaml/goyaml.v2"
|
||||
goyaml "go.yaml.in/yaml/v2"
|
||||
)
|
||||
|
||||
func sortSlice(s []interface{}) {
|
||||
for _, item := range s {
|
||||
if nestedMS, ok := item.(goyaml.MapSlice); ok {
|
||||
SortMapSlice(nestedMS)
|
||||
} else if nestedSlice, ok := item.([]interface{}); ok {
|
||||
sortSlice(nestedSlice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SortMapSlice recursively sorts the given goyaml.MapSlice by key.
|
||||
// It can be used in combination with Encode to sort YAML by key
|
||||
// before encoding it.
|
||||
|
@ -34,11 +44,7 @@ func SortMapSlice(ms goyaml.MapSlice) {
|
|||
if nestedMS, ok := item.Value.(goyaml.MapSlice); ok {
|
||||
SortMapSlice(nestedMS)
|
||||
} else if nestedSlice, ok := item.Value.([]interface{}); ok {
|
||||
for _, vItem := range nestedSlice {
|
||||
if nestedMS, ok := vItem.(goyaml.MapSlice); ok {
|
||||
SortMapSlice(nestedMS)
|
||||
}
|
||||
}
|
||||
sortSlice(nestedSlice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ import (
|
|||
"bytes"
|
||||
"testing"
|
||||
|
||||
goyaml "go.yaml.in/yaml/v2"
|
||||
"sigs.k8s.io/yaml"
|
||||
goyaml "sigs.k8s.io/yaml/goyaml.v2"
|
||||
)
|
||||
|
||||
func TestSortMapSlice(t *testing.T) {
|
||||
|
@ -124,6 +124,54 @@ func TestSortMapSlice(t *testing.T) {
|
|||
"r": "value-r",
|
||||
},
|
||||
"e": []interface{}{"strawberry", "banana"},
|
||||
"f": []interface{}{
|
||||
[]interface{}{
|
||||
map[string]interface{}{
|
||||
"f1": map[string]interface{}{
|
||||
"f1q": "value-f1q",
|
||||
"f1p": "value-f1p",
|
||||
"f1r": "value-f1r",
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"f2": map[string]interface{}{
|
||||
"f2q": "value-f2q",
|
||||
"f2p": "value-f2p",
|
||||
"f2r": "value-f2r",
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"f3": map[string]interface{}{
|
||||
"f3q": "value-f3q",
|
||||
"f3p": "value-f3p",
|
||||
"f3r": "value-f3r",
|
||||
},
|
||||
},
|
||||
},
|
||||
[]interface{}{
|
||||
map[string]interface{}{
|
||||
"F1": map[string]interface{}{
|
||||
"F1q": "value-F1q",
|
||||
"F1p": "value-F1p",
|
||||
"F1r": "value-F1r",
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"F2": map[string]interface{}{
|
||||
"F2q": "value-F2q",
|
||||
"F2p": "value-F2p",
|
||||
"F2r": "value-F2r",
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"F3": map[string]interface{}{
|
||||
"F3q": "value-F3q",
|
||||
"F3p": "value-F3p",
|
||||
"F3r": "value-F3r",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: map[string]interface{}{
|
||||
"a": map[string]interface{}{
|
||||
|
@ -146,6 +194,54 @@ func TestSortMapSlice(t *testing.T) {
|
|||
"r": "value-r",
|
||||
},
|
||||
"e": []interface{}{"strawberry", "banana"},
|
||||
"f": []interface{}{
|
||||
[]interface{}{
|
||||
map[string]interface{}{
|
||||
"f1": map[string]interface{}{
|
||||
"f1p": "value-f1p",
|
||||
"f1q": "value-f1q",
|
||||
"f1r": "value-f1r",
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"f2": map[string]interface{}{
|
||||
"f2p": "value-f2p",
|
||||
"f2q": "value-f2q",
|
||||
"f2r": "value-f2r",
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"f3": map[string]interface{}{
|
||||
"f3p": "value-f3p",
|
||||
"f3q": "value-f3q",
|
||||
"f3r": "value-f3r",
|
||||
},
|
||||
},
|
||||
},
|
||||
[]interface{}{
|
||||
map[string]interface{}{
|
||||
"F1": map[string]interface{}{
|
||||
"F1p": "value-F1p",
|
||||
"F1q": "value-F1q",
|
||||
"F1r": "value-F1r",
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"F2": map[string]interface{}{
|
||||
"F2p": "value-F2p",
|
||||
"F2q": "value-F2q",
|
||||
"F2r": "value-F2r",
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"F3": map[string]interface{}{
|
||||
"F3p": "value-F3p",
|
||||
"F3q": "value-F3q",
|
||||
"F3r": "value-F3r",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
rootCmd.SilenceUsage = true
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var pkgCmd = &cobra.Command{
|
||||
Use: "pkg",
|
||||
Short: "Tools for helping with Flux development tasks in the github.com/fluxcd/pkg repository",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(pkgCmd)
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
|
||||
"github.com/fluxcd/pkg/cmd/internal"
|
||||
)
|
||||
|
||||
var pkgPrepareReleaseCmd = &cobra.Command{
|
||||
Use: "prep",
|
||||
Short: "Prepare release for the github.com/fluxcd/pkg repository Go modules",
|
||||
RunE: runPrepareRelease,
|
||||
}
|
||||
|
||||
var pkgPrepareReleaseCmdFlags struct {
|
||||
yes bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
pkgCmd.AddCommand(pkgPrepareReleaseCmd)
|
||||
|
||||
pkgPrepareReleaseCmd.Flags().BoolVar(&pkgPrepareReleaseCmdFlags.yes, "yes", false,
|
||||
"Skip confirmation prompt and apply changes directly. Use with caution.")
|
||||
pkgPrepareReleaseCmd.Flags().MarkHidden("yes")
|
||||
}
|
||||
|
||||
func runPrepareRelease(cmd *cobra.Command, args []string) error {
|
||||
ctx := ctrl.SetupSignalHandler()
|
||||
|
||||
res, err := internal.ComputeModuleBumps(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compute module bumps: %w", err)
|
||||
}
|
||||
if !res.MustBumpInternalModules() {
|
||||
return nil
|
||||
}
|
||||
res.PrintBumps()
|
||||
|
||||
// Prompt for confirmation to apply changes.
|
||||
if !pkgPrepareReleaseCmdFlags.yes {
|
||||
fmt.Println("\nConfirm applying changes above to file system? (Y/n, only uppercase Y will confirm)")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
if response != "Y" {
|
||||
fmt.Println("Aborting changes.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Apply changes to the file system.
|
||||
if err := res.ApplyInternalBumps(ctx); err != nil {
|
||||
return fmt.Errorf("failed to apply module bumps: %w", err)
|
||||
}
|
||||
|
||||
// Show git status to the user.
|
||||
gitStatus := exec.CommandContext(ctx, "git", "status")
|
||||
gitStatus.Stdout = os.Stdout
|
||||
gitStatus.Stderr = os.Stderr
|
||||
return gitStatus.Run()
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
|
||||
"github.com/fluxcd/pkg/cmd/internal"
|
||||
)
|
||||
|
||||
var pkgReleaseCmd = &cobra.Command{
|
||||
Use: "release",
|
||||
Short: "Release the github.com/fluxcd/pkg repository Go modules",
|
||||
RunE: runRelease,
|
||||
}
|
||||
|
||||
var pkgReleaseCmdFlags struct {
|
||||
preview bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
pkgCmd.AddCommand(pkgReleaseCmd)
|
||||
|
||||
pkgReleaseCmd.Flags().BoolVar(&pkgReleaseCmdFlags.preview, "preview", false,
|
||||
"Preview the release changes without applying them.")
|
||||
}
|
||||
|
||||
func runRelease(cmd *cobra.Command, args []string) error {
|
||||
ctx := ctrl.SetupSignalHandler()
|
||||
|
||||
res, err := internal.ComputeModuleBumps(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compute module bumps: %w", err)
|
||||
}
|
||||
if res.MustBumpInternalModules() {
|
||||
res.PrintBumps()
|
||||
return errors.New("modules need to be bumped, please run 'make prep' first and open a pull request")
|
||||
}
|
||||
if !res.MustPushTags() {
|
||||
return nil
|
||||
}
|
||||
res.PrintTags()
|
||||
|
||||
if pkgReleaseCmdFlags.preview {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prompt for confirmation to push the tags.
|
||||
fmt.Println("\nConfirm pushing tags above to Git repository? (Y/n, only uppercase Y will confirm)")
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
if response != "Y" {
|
||||
fmt.Println("Aborting changes.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Push the tags to the remote repository.
|
||||
if err := res.PushTags(ctx); err != nil {
|
||||
return fmt.Errorf("failed to push tags: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "flux-tools",
|
||||
Short: "Tools for helping with Flux development tasks",
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
module github.com/fluxcd/pkg/cmd
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/spf13/cobra v1.9.1
|
||||
sigs.k8s.io/controller-runtime v0.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // 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.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/gnostic-models v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pjbgf/sha1cd v0.4.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.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/text v0.27.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.33.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.33.0 // indirect
|
||||
k8s.io/apimachinery v0.33.0 // indirect
|
||||
k8s.io/client-go v0.33.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
|
@ -0,0 +1,269 @@
|
|||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
|
||||
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
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=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.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/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
|
||||
github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/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/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
|
||||
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
|
||||
k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
|
||||
k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
|
||||
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
|
||||
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
|
||||
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
|
||||
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/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-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
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 internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/format/diff"
|
||||
)
|
||||
|
||||
// ComputeModuleBumps looks at the current Git repository and computes
|
||||
// the necessary module bumps based on the latest tags and changes in
|
||||
// the codebase. It returns the modules that need to be bumped and their
|
||||
// tags in the correct push order.
|
||||
func ComputeModuleBumps(ctx context.Context) (*ModuleBumps, error) {
|
||||
// Enumerate taggable and bumpable modules in the repository.
|
||||
taggables, err := EnumerateTaggableModules()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to enumerate taggable modules: %w", err)
|
||||
}
|
||||
bumpables := EnumerateBumpableModules(taggables)
|
||||
isTaggable := make(map[string]bool)
|
||||
for _, bumpable := range bumpables {
|
||||
if slices.Contains(taggables, bumpable) {
|
||||
isTaggable[bumpable] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Open the current Git repository.
|
||||
repo, err := git.PlainOpen(".")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open repository: %w", err)
|
||||
}
|
||||
|
||||
// Get iterator for tags in the repository.
|
||||
tagsIter, err := repo.Tags()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tags: %w", err)
|
||||
}
|
||||
defer tagsIter.Close()
|
||||
|
||||
// Collect tags for each module.
|
||||
moduleTags := make(map[string][]*semver.Version)
|
||||
err = tagsIter.ForEach(func(ref *plumbing.Reference) error {
|
||||
tag := ref.Name().Short()
|
||||
for _, module := range taggables {
|
||||
prefix := module + "/v"
|
||||
if !strings.HasPrefix(tag, prefix) {
|
||||
continue
|
||||
}
|
||||
v, err := semver.NewVersion(strings.TrimPrefix(tag, prefix))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
moduleTags[module] = append(moduleTags[module], v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to iterate over tags: %w", err)
|
||||
}
|
||||
|
||||
// Find latest for each module.
|
||||
moduleLatest := make(map[string]*semver.Version)
|
||||
for module, versions := range moduleTags {
|
||||
sort.Sort(sort.Reverse(semver.Collection(versions)))
|
||||
moduleLatest[module] = versions[0]
|
||||
}
|
||||
|
||||
// Find modules that have a diff between the latest tag and HEAD,
|
||||
// i.e. those that need to be bumped.
|
||||
var moduleBumps []*ModuleBump
|
||||
headRef, err := repo.Head()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get HEAD reference: %w", err)
|
||||
}
|
||||
headCommit, err := repo.CommitObject(headRef.Hash())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get HEAD commit: %w", err)
|
||||
}
|
||||
for _, taggable := range taggables {
|
||||
// Is it a new module?
|
||||
latest, ok := moduleLatest[taggable]
|
||||
if !ok {
|
||||
bump, err := NewModuleBumpForNewModule(taggable)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create module bump for new module %s: %w", taggable, err)
|
||||
}
|
||||
moduleBumps = append(moduleBumps, bump)
|
||||
continue
|
||||
}
|
||||
|
||||
// Compute the patch (the diff) between the latest tag and HEAD.
|
||||
tag := fmt.Sprintf("%s/v%s", taggable, latest.Original())
|
||||
tagRef, err := repo.Tag(tag)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tag %s: %w", tag, err)
|
||||
}
|
||||
tagObject, err := repo.TagObject(tagRef.Hash())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get commit for tag %s: %w", tag, err)
|
||||
}
|
||||
tagCommit, err := repo.CommitObject(tagObject.Target)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get commit for tag %s: %w", tag, err)
|
||||
}
|
||||
patch, err := tagCommit.PatchContext(ctx, headCommit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create patch between %s and HEAD: %w", tag, err)
|
||||
}
|
||||
|
||||
// For each file change in the patch, check if it belongs to the module.
|
||||
prefix := taggable + "/"
|
||||
fileChanged := func(f diff.File) bool {
|
||||
if f == nil || !strings.HasPrefix(f.Path(), prefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
// This loop is for removing bumps to <taggable> if the file
|
||||
// is of the form <taggable>/<other_module>/<file> where
|
||||
// <bumpable> is <taggable>/<other_module>.
|
||||
path := f.Path()
|
||||
for _, bumpable := range bumpables {
|
||||
if strings.HasPrefix(path, bumpable+"/") && strings.HasPrefix(bumpable, prefix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
changed := false
|
||||
for _, file := range patch.FilePatches() {
|
||||
from, to := file.Files()
|
||||
if fileChanged(from) || fileChanged(to) {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
bump, err := NewModuleBump(taggable, latest.Original(), latest.IncMinor().String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create module bump for %s: %w", taggable, err)
|
||||
}
|
||||
moduleBumps = append(moduleBumps, bump)
|
||||
}
|
||||
}
|
||||
|
||||
// For each taggable module that needs to receive a new release,
|
||||
// bump it inside other bumpable modules.
|
||||
targetModules := make([][]string, len(moduleBumps))
|
||||
mustBumpInternalModules := false
|
||||
for i := 0; i < len(moduleBumps); i++ { // moduleBumps grows dynamically inside this loop.
|
||||
bump := moduleBumps[i]
|
||||
for _, targetModule := range bumpables {
|
||||
if targetModule == bump.module {
|
||||
continue
|
||||
}
|
||||
ok, err := bump.DryRunApply(ctx, targetModule)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to apply bump %s to module %s: %w", bump, targetModule, err)
|
||||
}
|
||||
if ok {
|
||||
targetModules[i] = append(targetModules[i], targetModule)
|
||||
|
||||
// After updating the targetModule, if targetModule is taggable,
|
||||
// then it must be bumped as well.
|
||||
if !isTaggable[targetModule] {
|
||||
continue
|
||||
}
|
||||
willBeBumpedAlready := false
|
||||
for _, existingBump := range moduleBumps {
|
||||
if targetModule == existingBump.module {
|
||||
willBeBumpedAlready = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !willBeBumpedAlready {
|
||||
var newBump *ModuleBump
|
||||
latest, ok := moduleLatest[targetModule]
|
||||
if !ok {
|
||||
// This is a new module that was never tagged before.
|
||||
newBump, err = NewModuleBumpForNewModule(targetModule)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create module bump for new module %s: %w", targetModule, err)
|
||||
}
|
||||
} else {
|
||||
// This is an existing module that already has a tag.
|
||||
newBump, err = NewModuleBump(targetModule, latest.Original(), latest.IncMinor().String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create module bump for %s: %w", targetModule, err)
|
||||
}
|
||||
}
|
||||
moduleBumps = append(moduleBumps, newBump)
|
||||
targetModules = append(targetModules, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(targetModules[i]) > 0 {
|
||||
mustBumpInternalModules = true
|
||||
}
|
||||
}
|
||||
|
||||
// If bumps must be applied, return early without computing the order of tags to push.
|
||||
if mustBumpInternalModules {
|
||||
return &ModuleBumps{
|
||||
bumps: moduleBumps,
|
||||
targetModules: targetModules,
|
||||
mustBumpInternalModules: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Compute topological order of tags to push using a depth-first search.
|
||||
// https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
|
||||
const (
|
||||
unmarked = iota
|
||||
permanentMark
|
||||
temporaryMark
|
||||
)
|
||||
mark := make([]int, len(moduleBumps))
|
||||
tagsToPush := make([]string, 0, len(moduleBumps))
|
||||
var depthFirstSearch func(i int) // Recursive closures need to be defined like this.
|
||||
depthFirstSearch = func(i int) {
|
||||
if mark[i] == permanentMark {
|
||||
return
|
||||
}
|
||||
if mark[i] == temporaryMark {
|
||||
// Should never happen, as cycles are not allowed in Go modules dependencies.
|
||||
panic("cycle detected in module bumps")
|
||||
}
|
||||
mark[i] = temporaryMark
|
||||
for _, targetModule := range targetModules[i] {
|
||||
// Find which j represents targetModule in moduleBumps.
|
||||
for j, targetBump := range moduleBumps {
|
||||
if targetBump.module == targetModule {
|
||||
depthFirstSearch(j)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
mark[i] = permanentMark
|
||||
tag := fmt.Sprintf("%s/v%s", moduleBumps[i].module, moduleBumps[i].newVersion)
|
||||
tagsToPush = append(tagsToPush, tag)
|
||||
}
|
||||
for i := range moduleBumps {
|
||||
depthFirstSearch(i)
|
||||
}
|
||||
|
||||
return &ModuleBumps{
|
||||
bumps: moduleBumps,
|
||||
targetModules: targetModules,
|
||||
mustBumpInternalModules: false,
|
||||
tagsToPush: tagsToPush,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
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 internal
|
||||
|
||||
// EnumerateBumpableModules returns a list of Go modules that can be bumped
|
||||
// in the github.com/fluxcd/pkg repository. This includes all modules that
|
||||
// are taggable, as well as specific modules that are not tagged but should
|
||||
// still be considered for version bumps.
|
||||
func EnumerateBumpableModules(taggable []string) []string {
|
||||
return append(taggable, testModules...)
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
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 internal
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EnumerateTaggableModules traverses the current directory and returns the
|
||||
// paths of directories containing Go modules that are taggable for release.
|
||||
func EnumerateTaggableModules() ([]string, error) {
|
||||
var nonTaggables = append([]string{"cmd"}, testModules...)
|
||||
var taggables []string
|
||||
err := fs.WalkDir(os.DirFS("."), ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
for _, nonTaggable := range nonTaggables {
|
||||
if path == nonTaggable || strings.HasPrefix(path, nonTaggable+"/") {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
f, err := os.Open(filepath.Join(path, "go.mod"))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
f.Close()
|
||||
taggables = append(taggables, path)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slices.Sort(taggables)
|
||||
return taggables, nil
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
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 internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// ModuleBump represents a module version bump operation and
|
||||
// helps applying the bump to target modules in the repository.
|
||||
type ModuleBump struct {
|
||||
module string
|
||||
oldVersion string
|
||||
newVersion string
|
||||
regex *regexp.Regexp
|
||||
}
|
||||
|
||||
// NewModuleBump creates a ModuleBump for the given module with the old and new versions.
|
||||
func NewModuleBump(module, oldVersion, newVersion string) (*ModuleBump, error) {
|
||||
pattern := fmt.Sprintf(`github\.com/fluxcd/pkg/%s v([^\s]+)`, module)
|
||||
regex, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compile regex for module %s: %w", module, err)
|
||||
}
|
||||
return &ModuleBump{
|
||||
module: module,
|
||||
oldVersion: oldVersion,
|
||||
newVersion: newVersion,
|
||||
regex: regex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewModuleBumpForNewModule creates a ModuleBump for a new module with an initial version.
|
||||
func NewModuleBumpForNewModule(module string) (*ModuleBump, error) {
|
||||
return NewModuleBump(module, "", "0.1.0")
|
||||
}
|
||||
|
||||
// String implements the fmt.Stringer interface for *ModuleBump.
|
||||
func (m *ModuleBump) String() string {
|
||||
from := "new module"
|
||||
if m.oldVersion != "" {
|
||||
from = m.oldVersion
|
||||
}
|
||||
return fmt.Sprintf("%s: %s => %s", m.module, from, m.newVersion)
|
||||
}
|
||||
|
||||
// Apply replaces the module version in the given target module.
|
||||
func (m *ModuleBump) Apply(ctx context.Context, targetModule string) (bool, error) {
|
||||
const dryRun = false
|
||||
return m.apply(ctx, targetModule, dryRun)
|
||||
}
|
||||
|
||||
// DryRunApply replaces the module version in the given target module without writing changes.
|
||||
func (m *ModuleBump) DryRunApply(ctx context.Context, targetModule string) (bool, error) {
|
||||
const dryRun = true
|
||||
return m.apply(ctx, targetModule, dryRun)
|
||||
}
|
||||
|
||||
// apply replaces the module version in the given target module.
|
||||
func (m *ModuleBump) apply(ctx context.Context, targetModule string, dryRun bool) (bool, error) {
|
||||
gomod := fmt.Sprintf("%s/go.mod", targetModule)
|
||||
b, err := os.ReadFile(gomod)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to read %s: %w", gomod, err)
|
||||
}
|
||||
oldContent := string(b)
|
||||
if !m.regex.MatchString(oldContent) {
|
||||
return false, nil
|
||||
}
|
||||
bumpString := fmt.Sprintf("github.com/fluxcd/pkg/%s v%s", m.module, m.newVersion)
|
||||
newContent := m.regex.ReplaceAllString(oldContent, bumpString)
|
||||
if oldContent == newContent {
|
||||
return false, nil
|
||||
}
|
||||
if !dryRun {
|
||||
if err := os.WriteFile(gomod, []byte(newContent), 0644); err != nil {
|
||||
return false, fmt.Errorf("failed to write %s: %w", gomod, err)
|
||||
}
|
||||
gomodtidy := exec.CommandContext(ctx, "go", "mod", "tidy")
|
||||
gomodtidy.Dir = targetModule
|
||||
b, err = gomodtidy.CombinedOutput()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to run go mod tidy in %s: %w\n%s", targetModule, err, string(b))
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
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 internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ModuleBumps represents a collection of module bumps
|
||||
// that need to be applied to the repository.
|
||||
type ModuleBumps struct {
|
||||
// bumps are the Go modules that need to be released because
|
||||
// there is a diff between the latest tag and HEAD.
|
||||
bumps []*ModuleBump
|
||||
|
||||
// targetModules[i][j] is the j-th module that needs to receive
|
||||
// the i-th bump, i.e. bumps[i].
|
||||
targetModules [][]string
|
||||
|
||||
// mustBumpInternalModules is true if at least one module needs to receive at least one bump.
|
||||
mustBumpInternalModules bool
|
||||
|
||||
// tagsToPush are the tags that need to be pushed to the remote repository
|
||||
// in the topological order.
|
||||
tagsToPush []string
|
||||
}
|
||||
|
||||
// MustBumpInternalModules returns true if there are any module bumps that need to be applied
|
||||
// to the current repository.
|
||||
func (m *ModuleBumps) MustBumpInternalModules() bool {
|
||||
return m.mustBumpInternalModules
|
||||
}
|
||||
|
||||
// PrintBumps prints the module bumps result in a human-readable format.
|
||||
func (m *ModuleBumps) PrintBumps() {
|
||||
for i, bump := range m.bumps {
|
||||
if len(m.targetModules[i]) == 0 {
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Bumped %s in modules: %s\n", bump, strings.Join(m.targetModules[i], ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyInternalBumps applies the module bumps to the file system.
|
||||
func (m *ModuleBumps) ApplyInternalBumps(ctx context.Context) error {
|
||||
for i, bump := range m.bumps {
|
||||
for _, targetModule := range m.targetModules[i] {
|
||||
if _, err := bump.Apply(ctx, targetModule); err != nil {
|
||||
return fmt.Errorf("failed to apply bump %s to module %s: %w", bump, targetModule, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MustPushTags tells whether the tags need to be pushed to the remote repository.
|
||||
func (m *ModuleBumps) MustPushTags() bool {
|
||||
return len(m.tagsToPush) > 0
|
||||
}
|
||||
|
||||
// PrintTags prints the tags that will be pushed to the remote repository.
|
||||
func (m *ModuleBumps) PrintTags() {
|
||||
for _, tag := range m.tagsToPush {
|
||||
fmt.Println(tag)
|
||||
}
|
||||
}
|
||||
|
||||
// PushTags pushes the tags to the remote repository.
|
||||
func (m *ModuleBumps) PushTags(ctx context.Context) error {
|
||||
for _, tag := range m.tagsToPush {
|
||||
tagCmd := exec.CommandContext(ctx, "git", "tag", "-s", "-m", tag, tag)
|
||||
tagCmd.Stdout = os.Stdout
|
||||
tagCmd.Stderr = os.Stderr
|
||||
if err := tagCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to create tag %s: %w", tag, err)
|
||||
}
|
||||
pushCmd := exec.CommandContext(ctx, "git", "push", "origin", tag)
|
||||
pushCmd.Stdout = os.Stdout
|
||||
pushCmd.Stderr = os.Stderr
|
||||
if err := pushCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to push tag %s: %w", tag, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
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 internal
|
||||
|
||||
// testModules contains the paths of modules that are used for testing
|
||||
// only and should not be tagged.
|
||||
var testModules = []string{
|
||||
"git/internal/e2e",
|
||||
"oci/tests/integration",
|
||||
}
|
|
@ -1,6 +1,31 @@
|
|||
# Release
|
||||
# Release Documentation
|
||||
|
||||
To release a new version of a package the following steps should be followed:
|
||||
This process is intended to be run locally, in the clone of a Flux maintainer,
|
||||
properly configured with commit signing and GitHub credentials.
|
||||
|
||||
1. Run `make release-<package name> VER=<next semver>`.
|
||||
1. Confirm CI builds and releases the newly tagged version.
|
||||
First, a preparation PR must be created bumping all the Go modules
|
||||
from this repository that have changed since their latest version.
|
||||
|
||||
Run the following commands:
|
||||
|
||||
1. `git checkout main`
|
||||
2. `git pull`
|
||||
3. `make prep`
|
||||
|
||||
If there are any changes, commit, open a PR `Prepare for release` and merge.
|
||||
|
||||
If no changes are needed, then:
|
||||
|
||||
1. `git checkout main`
|
||||
2. `git pull`
|
||||
3. `make release`
|
||||
|
||||
Both `make` commands will show a plan of the changes they
|
||||
will make and ask for confirmation.
|
||||
|
||||
## New test Go modules
|
||||
|
||||
Whenever adding new test Go modules like `git/internal/e2e` or `oci/tests/integration`,
|
||||
you must also add the module path to the `testModules` slice in `cmd/internal/test_modules.go`.
|
||||
This is necessary to ensure that these modules are not considered for tagging when running
|
||||
the `make release` command.
|
||||
|
|
|
@ -19,6 +19,7 @@ package github
|
|||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -33,10 +34,10 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
AppIDKey = "githubAppID"
|
||||
AppInstallationIDKey = "githubAppInstallationID"
|
||||
AppPrivateKey = "githubAppPrivateKey"
|
||||
AppBaseUrlKey = "githubAppBaseURL"
|
||||
KeyAppID = "githubAppID"
|
||||
KeyAppInstallationID = "githubAppInstallationID"
|
||||
KeyAppPrivateKey = "githubAppPrivateKey"
|
||||
KeyAppBaseURL = "githubAppBaseURL"
|
||||
|
||||
AccessTokenUsername = "x-access-token"
|
||||
)
|
||||
|
@ -54,6 +55,7 @@ type Client struct {
|
|||
name string
|
||||
namespace string
|
||||
operation string
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
// OptFunc enables specifying options for the provider.
|
||||
|
@ -67,6 +69,9 @@ func New(opts ...OptFunc) (*Client, error) {
|
|||
}
|
||||
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
if p.tlsConfig != nil {
|
||||
transport.TLSClientConfig = p.tlsConfig
|
||||
}
|
||||
if p.proxyURL != nil {
|
||||
proxyStr := p.proxyURL.String()
|
||||
proxyConfig := &httpproxy.Config{
|
||||
|
@ -111,51 +116,29 @@ func New(opts ...OptFunc) (*Client, error) {
|
|||
return p, nil
|
||||
}
|
||||
|
||||
// WithInstallationID configures the installation ID of the GitHub App.
|
||||
func WithInstllationID(installationID string) OptFunc {
|
||||
// WithTLSConfig sets the tls config to use with the transport.
|
||||
func WithTLSConfig(tlsConfig *tls.Config) OptFunc {
|
||||
return func(p *Client) {
|
||||
p.installationID = installationID
|
||||
}
|
||||
}
|
||||
|
||||
// WithAppID configures the app ID of the GitHub App.
|
||||
func WithAppID(appID string) OptFunc {
|
||||
return func(p *Client) {
|
||||
p.appID = appID
|
||||
}
|
||||
}
|
||||
|
||||
// WithPrivateKey configures the private key of the GitHub App.
|
||||
func WithPrivateKey(pk []byte) OptFunc {
|
||||
return func(p *Client) {
|
||||
p.privateKey = pk
|
||||
}
|
||||
}
|
||||
|
||||
// WithAppBaseURL configures the GitHub API endpoint to use to fetch GitHub App
|
||||
// installation token.
|
||||
func WithAppBaseURL(appBaseURL string) OptFunc {
|
||||
return func(p *Client) {
|
||||
p.apiURL = appBaseURL
|
||||
p.tlsConfig = tlsConfig
|
||||
}
|
||||
}
|
||||
|
||||
// WithAppData configures the client using data from a map
|
||||
func WithAppData(appData map[string][]byte) OptFunc {
|
||||
return func(p *Client) {
|
||||
val, ok := appData[AppIDKey]
|
||||
val, ok := appData[KeyAppID]
|
||||
if ok {
|
||||
p.appID = string(val)
|
||||
}
|
||||
val, ok = appData[AppInstallationIDKey]
|
||||
val, ok = appData[KeyAppInstallationID]
|
||||
if ok {
|
||||
p.installationID = string(val)
|
||||
}
|
||||
val, ok = appData[AppPrivateKey]
|
||||
val, ok = appData[KeyAppPrivateKey]
|
||||
if ok {
|
||||
p.privateKey = val
|
||||
}
|
||||
val, ok = appData[AppBaseUrlKey]
|
||||
val, ok = appData[KeyAppBaseURL]
|
||||
if ok {
|
||||
p.apiURL = string(val)
|
||||
}
|
||||
|
@ -249,10 +232,10 @@ func GetCredentials(ctx context.Context, opts ...OptFunc) (string, string, error
|
|||
|
||||
func (p *Client) buildCacheKey() string {
|
||||
keyParts := []string{
|
||||
fmt.Sprintf("%s=%s", AppIDKey, p.appID),
|
||||
fmt.Sprintf("%s=%s", AppInstallationIDKey, p.installationID),
|
||||
fmt.Sprintf("%s=%s", AppBaseUrlKey, p.apiURL),
|
||||
fmt.Sprintf("%s=%s", AppPrivateKey, string(p.privateKey)),
|
||||
fmt.Sprintf("%s=%s", KeyAppID, p.appID),
|
||||
fmt.Sprintf("%s=%s", KeyAppInstallationID, p.installationID),
|
||||
fmt.Sprintf("%s=%s", KeyAppBaseURL, p.apiURL),
|
||||
fmt.Sprintf("%s=%s", KeyAppPrivateKey, string(p.privateKey)),
|
||||
}
|
||||
rawKey := strings.Join(keyParts, ",")
|
||||
hash := sha256.Sum256([]byte(rawKey))
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue