Compare commits

...

40 Commits

Author SHA1 Message Date
Matheus Pimenta 4ca8fb011f
Merge pull request #1007 from cappyzawa/remove-servername-pinning
runtime/secrets: remove ServerName pinning from TLS config
2025-08-14 14:36:26 +01:00
cappyzawa 8ce7a0dc67
Prepare for release
Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-08-14 22:04:01 +09:00
cappyzawa a254eaccd7
runtime/secrets: remove ServerName pinning from TLS config
Remove ServerName pinning functionality that can cause TLS
verification failures in production environments with redirects,
proxies, and multi-host scenarios.

The Go standard library automatically handles SNI and hostname
verification based on the actual connection target, providing
better compatibility and security than fixed ServerName values.

Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-08-14 22:03:28 +09:00
Stefan Prodan 6bf77f094c
Merge pull request #1006 from fluxcd/oci-rename-util
oci: Expose `RenameWithFallback` utility
2025-08-12 13:37:27 +03:00
Stefan Prodan b0abad5db0
oci: Expose `RenameWithFallback` utility
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2025-08-12 13:21:31 +03:00
Stefan Prodan 621a899e4c
Merge pull request #1005 from fluxcd/dependabot/github_actions/ci-914c47a322
build(deps): bump the ci group across 1 directory with 7 updates
2025-08-12 08:47:31 +03:00
dependabot[bot] 2c7a66601d
build(deps): bump the ci group across 1 directory with 7 updates
Bumps the ci group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `4.2.2` | `5.0.0` |
| [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) | `4.2.1` | `4.3.1` |
| [actions/cache](https://github.com/actions/cache) | `4.2.3` | `4.2.4` |
| [google-github-actions/auth](https://github.com/google-github-actions/auth) | `2.1.10` | `2.1.12` |
| [google-github-actions/setup-gcloud](https://github.com/google-github-actions/setup-gcloud) | `2.1.4` | `2.2.0` |
| [docker/login-action](https://github.com/docker/login-action) | `3.4.0` | `3.5.0` |
| [github/codeql-action](https://github.com/github/codeql-action) | `3.29.2` | `3.29.8` |



Updates `actions/checkout` from 4.2.2 to 5.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](11bd71901b...08c6903cd8)

Updates `aws-actions/configure-aws-credentials` from 4.2.1 to 4.3.1
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](b475783126...7474bc4690)

Updates `actions/cache` from 4.2.3 to 4.2.4
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](5a3ec84eff...0400d5f644)

Updates `google-github-actions/auth` from 2.1.10 to 2.1.12
- [Release notes](https://github.com/google-github-actions/auth/releases)
- [Changelog](https://github.com/google-github-actions/auth/blob/main/CHANGELOG.md)
- [Commits](ba79af0395...b7593ed2ef)

Updates `google-github-actions/setup-gcloud` from 2.1.4 to 2.2.0
- [Release notes](https://github.com/google-github-actions/setup-gcloud/releases)
- [Changelog](https://github.com/google-github-actions/setup-gcloud/blob/main/CHANGELOG.md)
- [Commits](77e7a554d4...cb1e50a993)

Updates `docker/login-action` from 3.4.0 to 3.5.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](74a5d14239...184bdaa072)

Updates `github/codeql-action` from 3.29.2 to 3.29.8
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](181d5eefc2...76621b61de)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: ci
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: 4.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
- dependency-name: actions/cache
  dependency-version: 4.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ci
- dependency-name: google-github-actions/auth
  dependency-version: 2.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ci
- dependency-name: google-github-actions/setup-gcloud
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
- dependency-name: docker/login-action
  dependency-version: 3.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ci
- dependency-name: github/codeql-action
  dependency-version: 3.29.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ci
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 04:34:29 +00:00
Matheus Pimenta 4bb23ecbdb
Merge pull request #1002 from dipti-pai/azure-privatecloud-oidc
auth/azure: Add support for custom Azure cloud configurations
2025-08-07 19:39:47 +01:00
Dipti Pai d6fbf47b79 auth/azure: Add support for custom Azure cloud configurations
- Read Azure endpoint configuration from JSON file.
- JSON file path is configured via AZURE_ENVIRONMENT_FILEPATH env variable.
- Unit tests.

Signed-off-by: Dipti Pai <diptipai89@outlook.com>
2025-08-07 11:23:52 -07:00
Matheus Pimenta a849bcf3b7
Merge pull request #1004 from fluxcd/auth-hc-refactor
auth: Refactor usage of http client from options
2025-08-07 16:55:43 +01:00
Matheus Pimenta a795fdc737
auth: Refactor usage of http client from options
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
2025-08-07 15:59:34 +01:00
Matheus Pimenta 11982f6ab4
Merge pull request #1003 from cappyzawa/feat/http-client-timeout
auth: add 10-second timeout to HTTP client for token acquisition
2025-08-07 11:08:12 +01:00
cappyzawa a2c5712e16
Prepare for release
Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-08-07 10:42:03 +09:00
cappyzawa 48209adba5
auth: add 10-second timeout to HTTP client for token acquisition
Improve the GetHTTPClient() method to always return an HTTP client
with a 10-second timeout, preventing indefinite hangs during token
acquisition from cloud provider STS services.

The oauth2 library may not properly handle context cancellation
internally, so setting a timeout at the HTTP client level provides
reliable protection against indefinite hangs when making requests
to cloud provider endpoints for token exchange.

This ensures token acquisition completes within a reasonable time
frame while maintaining existing proxy configuration support.

Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-08-07 10:34:13 +09:00
Matheus Pimenta f227e67fdd
Merge pull request #1001 from cappyzawa/feat/github-app-support-authmethodsfromsecret
runtime/secrets: add GitHub App support to AuthMethodsFromSecret
2025-08-06 13:03:29 +01:00
cappyzawa 5f5e254bd2
Prepare for release
Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-08-06 14:48:09 +09:00
cappyzawa c3282981b7
runtime/secrets: add GitHub App support to AuthMethodsFromSecret
Enable unified GitHub App authentication handling across Flux controllers
by integrating GitHub App support into the existing AuthMethodsFromSecret
pattern used by other controllers.

This improves consistency and reduces code duplication when controllers
need to handle GitHub App authentication. The implementation uses a type
alias for direct compatibility with existing pkg/git/github APIs while
maintaining package independence.

Remove unused GitHubAppAuth struct to avoid confusion with the new
GitHubAppData type alias.

Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-08-05 20:50:59 +09:00
Matheus Pimenta f01e8d6848
Merge pull request #999 from abhijith-darshan/feat/gh_app_ca
git/github: Add support for mTLS to GitHub App transport
2025-08-02 21:39:16 +01:00
Matheus Pimenta e77a11bc22
Prepare for release
Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
2025-08-02 21:19:00 +01:00
abhijith-darshan 9fd9628968
git/github: Add support for mTLS to GitHub App transport
This commit ensures if available, a custom ca.crt is appended to system cert pool and set to the github app transport tls configuration

Signed-off-by: abhijith-darshan <abhijith.darshan@hotmail.com>

Signed-off-by: Matheus Pimenta <matheuscscp@gmail.com>
2025-08-02 21:17:30 +01:00
Matheus Pimenta 04d916d0c5
Merge pull request #997 from cappyzawa/feat/tls-config-from-secret-ref-options
runtime/secrets: add TLSConfigOption support to TLSConfigFromSecretRef
2025-07-29 23:46:30 +01:00
cappyzawa 4642dabf28
Prepare for release
Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-07-30 07:19:18 +09:00
cappyzawa 27b414e80f
runtime/secrets: add TLSConfigOption support to TLSConfigFromSecretRef
Add TLSConfigOption support to TLSConfigFromSecretRef function to
maintain consistency with TLSConfigFromSecret. This enables the same
configuration options (like WithSystemCertPool) for both functions.

The change maintains backward compatibility by using variadic parameters.
Also added test coverage for the new functionality.

Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-07-30 07:17:36 +09:00
Matheus Pimenta dbf1d227b0
Merge pull request #995 from cappyzawa/feat/tls-config-options
runtime/secrets: add WithSystemCertPool for CA handling
2025-07-29 17:43:51 +01:00
cappyzawa cb022f764d
Prepare for release
Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-07-30 01:27:10 +09:00
cappyzawa 488ca0955a
runtime/secrets: add WithSystemCertPool for CA handling
Flux controllers require different CA certificate handling modes:
- Replace mode (default): trust only user-provided CA certificates
- Extend mode: trust both system and user-provided CA certificates

This change adds functional options to TLSConfigFromSecret and
AuthMethodsFromSecret to support both modes. The default behavior
remains unchanged (replace mode) for security, while controllers
can opt-in to extend mode using WithSystemCertPool().

This enables unified secret handling across all Flux components
while maintaining backward compatibility and controller-specific
CA trust requirements.

Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-07-30 01:26:30 +09:00
Matheus Pimenta 1c8c7bb531
Merge pull request #993 from cappyzawa/feat/remove-insecure-parameter
runtime/secrets: remove insecure parameter from TLS funcs
2025-07-28 16:00:07 +01:00
cappyzawa e98aecf00e
Prepare for release
Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-07-28 23:40:32 +09:00
cappyzawa b59d18da0a
runtime/secrets: remove insecure parameter from TLS funcs
Remove the insecure boolean parameter from TLSConfigFromSecret
and TLSConfigFromSecretRef functions to enforce Flux security
policy that prohibits bypassing TLS certificate validation.

Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-07-28 23:39:49 +09:00
Stefan Prodan 44b53bd4ae
Merge pull request #992 from fluxcd/ssa-manager-exact-match
ssa: Add exact match option to field manager removal
2025-07-28 12:42:34 +03:00
Stefan Prodan ff888a4ac7
ssa: Add exact match option to field manager removal
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
2025-07-28 00:21:32 +03:00
Matheus Pimenta 95760a79c9
Merge pull request #991 from cappyzawa/feat/runtime-secrets-targeturl-insecure
runtime/secrets: add targetURL and insecure to TLS functions
2025-07-21 15:38:06 +01:00
cappyzawa 60b1dea324
Prepare for release
Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-07-21 00:30:30 +09:00
cappyzawa 7930b7b806
runtime/secrets: add targetURL and insecure to TLS functions
Add targetURL and insecure parameters to TLSConfigFromSecret and
TLSConfigFromSecretRef to resolve ServerName regression issues in
virtual hosting environments.

The migration to runtime/secrets caused TLS handshake failures
because Go cannot automatically set ServerName when custom
tls.Config is provided to http.Transport.TLSClientConfig.

This change enables proper SNI support by extracting hostname
from targetURL and setting tlsConfig.ServerName. The insecure
parameter provides consistent InsecureSkipVerify handling across
all Flux controllers.

Implement Functional Options Pattern for AuthMethodsFromSecret
with WithTLS() option to support mixed authentication scenarios
while maintaining backward compatibility through default values.

Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-07-21 00:27:20 +09:00
Matheus Pimenta ea0856db0e
Merge pull request #990 from ymettier/bug-nested-map-slice-slice
chartutil: Fix sort not recursing for slices
2025-07-19 13:32:17 +01:00
Yves Mettier 525798301f chartutil: Fix sort not recursing for slices
Signed-off-by: Yves Mettier <ymettier@free.fr>
2025-07-19 13:11:15 +01:00
Matheus Pimenta 4f64822f3a
Merge pull request #989 from cappyzawa/feat/runtime-secrets-token-auth-support
runtime/secrets: add TokenAuth detection support
2025-07-18 15:44:06 +01:00
cappyzawa a75690c399
Prepare for release
Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-07-18 23:25:14 +09:00
cappyzawa 447804609f
runtime/secrets: add TokenAuth detection support
Add TokenAuth detection to AuthMethodsFromSecret. TokenAuth uses "token"
key while BearerAuth uses "bearerToken" key, enabling unified token-based
authentication through runtime/secrets.

Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-07-18 20:17:34 +09:00
cappyzawa 754624dafa
runtime/secrets: simplify BearerAuth to string type
BearerAuth represents a single string value and doesn't need struct
wrapping. This change improves memory efficiency and aligns with Go
idioms for simple value types.

Signed-off-by: cappyzawa <cappyzawa@gmail.com>
2025-07-18 20:17:34 +09:00
38 changed files with 1238 additions and 172 deletions

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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@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

View File

@ -25,7 +25,7 @@ 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@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
with:
@ -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@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.8
with:
sarif_file: results.sarif

View File

@ -10,7 +10,7 @@ jobs:
contents: read
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Setup Go

View File

@ -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@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
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@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
uses: github/codeql-action/autobuild@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.8
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
uses: github/codeql-action/analyze@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.8

View File

@ -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

View File

@ -37,9 +37,10 @@ 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.GetAccessToken(c.ctx, Provider{}, c.opts...)
if err != nil {

View File

@ -54,7 +54,9 @@ func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (
var o auth.Options
o.Apply(opts...)
var confOpts []func(*config.LoadOptions) error
confOpts := []func(*config.LoadOptions) error{
config.WithHTTPClient(o.GetHTTPClient()),
}
stsRegion := o.STSRegion
if stsRegion == "" {
@ -77,10 +79,6 @@ func (p Provider) NewControllerToken(ctx context.Context, opts ...auth.Option) (
confOpts = append(confOpts, config.WithBaseEndpoint(e))
}
if hc := o.GetHTTPClient(); hc != nil {
confOpts = append(confOpts, config.WithHTTPClient(hc))
}
conf, err := p.impl().LoadDefaultConfig(ctx, confOpts...)
if err != nil {
return nil, err
@ -134,7 +132,8 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
roleSessionName := getRoleSessionName(serviceAccount, stsRegion)
stsOpts := sts.Options{
Region: stsRegion,
Region: stsRegion,
HTTPClient: o.GetHTTPClient(),
}
if e := o.STSEndpoint; e != "" {
@ -144,10 +143,6 @@ func (p Provider) NewTokenForServiceAccount(ctx context.Context, oidcToken strin
stsOpts.BaseEndpoint = &e
}
if hc := o.GetHTTPClient(); hc != nil {
stsOpts.HTTPClient = hc
}
req := &sts.AssumeRoleWithWebIdentityInput{
RoleArn: &roleARN,
RoleSessionName: &roleSessionName,
@ -243,10 +238,7 @@ func (p Provider) NewArtifactRegistryCredentials(ctx context.Context, registryIn
conf := aws.Config{
Region: getECRRegionFromRegistryInput(registryInput),
Credentials: accessToken.(*Credentials).provider(),
}
if hc := o.GetHTTPClient(); hc != nil {
conf.HTTPClient = hc
HTTPClient: o.GetHTTPClient(),
}
respAny, err := authTokenFunc(ctx, conf)
@ -346,9 +338,7 @@ func (p Provider) NewRESTConfig(ctx context.Context, accessTokens []auth.Token,
eksOpts := eks.Options{
Region: region,
Credentials: creds,
}
if hc != nil {
eksOpts.HTTPClient = hc
HTTPClient: hc,
}
clusterResource, err := p.impl().DescribeCluster(ctx, describeInput, eksOpts)
if err != nil {
@ -391,6 +381,7 @@ func (p Provider) NewRESTConfig(ctx context.Context, accessTokens []auth.Token,
stsOpts := sts.Options{
Region: region,
Credentials: creds,
HTTPClient: hc,
}
if e := o.STSEndpoint; e != "" {
if err := ValidateSTSEndpoint(e); err != nil {
@ -398,9 +389,6 @@ func (p Provider) NewRESTConfig(ctx context.Context, accessTokens []auth.Token,
}
stsOpts.BaseEndpoint = &e
}
if hc != nil {
stsOpts.HTTPClient = hc
}
presignedReq, err := p.impl().PresignGetCallerIdentity(ctx, presignOpts, stsOpts)
if err != nil {
return nil, fmt.Errorf("failed to presign GetCallerIdentity request: %w", err)

View File

@ -17,9 +17,12 @@ limitations under the License.
package azure
import (
"encoding/json"
"fmt"
"os"
"regexp"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
corev1 "k8s.io/api/core/v1"
)
@ -66,3 +69,80 @@ func parseCluster(cluster string) (string, string, string, error) {
clusterName := m[3]
return subscriptionID, resourceGroup, clusterName, nil
}
// envVarAzureEnvironmentFilepath is the environment variable name used to specify the path of the configuration file with custom Azure endpoints.
const envVarAzureEnvironmentFilepath = "AZURE_ENVIRONMENT_FILEPATH"
// 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 {
return nil, err
}
env := &Environment{}
if err = json.Unmarshal(content, env); err != nil {
return nil, err
}
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
}

View File

@ -23,6 +23,7 @@ import (
"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"
@ -54,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
@ -102,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) {
@ -136,6 +137,12 @@ func (p Provider) GetAccessTokenOptionsForArtifactRepository(artifactRepository
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"):
@ -161,13 +168,27 @@ func (Provider) ParseArtifactRepository(artifactRepository string) (string, erro
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 host is required.
return registry, 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.
@ -179,9 +200,10 @@ func (p Provider) NewArtifactRegistryCredentials(ctx context.Context, registry s
// Create the ACR authentication client.
endpoint := fmt.Sprintf("https://%s", registry)
var clientOpts azcontainerregistry.AuthenticationClientOptions
if hc := o.GetHTTPClient(); hc != nil {
clientOpts.Transport = hc
clientOpts := azcontainerregistry.AuthenticationClientOptions{
ClientOptions: azcore.ClientOptions{
Transport: o.GetHTTPClient(),
},
}
client, err := azcontainerregistry.NewAuthenticationClient(endpoint, &clientOpts)
if err != nil {
@ -237,6 +259,12 @@ func (Provider) GetAccessTokenOptionsForCluster(opts ...auth.Option) ([][]auth.O
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"):
@ -275,9 +303,10 @@ func (p Provider) NewRESTConfig(ctx context.Context, accessTokens []auth.Token,
}
// Create client for describing the cluster resource.
var clientOpts arm.ClientOptions
if hc := o.GetHTTPClient(); hc != nil {
clientOpts.Transport = hc
clientOpts := arm.ClientOptions{
ClientOptions: azcore.ClientOptions{
Transport: o.GetHTTPClient(),
},
}
client, err := p.impl().NewManagedClustersClient(
subscriptionID, armToken.credential(), &clientOpts)

View File

@ -22,6 +22,7 @@ import (
"crypto/rsa"
"fmt"
"net/url"
"os"
"testing"
"time"
@ -236,9 +237,10 @@ 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/repo",
@ -272,10 +274,32 @@ func TestProvider_ParseArtifactRegistry(t *testing.T) {
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 {
@ -289,6 +313,67 @@ 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
@ -543,6 +628,31 @@ func TestProvider_GetAccessTokenOptionsForCluster(t *testing.T) {
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"),
@ -580,3 +690,21 @@ users:
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
}

View File

@ -38,9 +38,10 @@ 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 {

View File

@ -55,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 {
@ -145,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 {

View File

@ -19,6 +19,7 @@ package auth
import (
"net/http"
"net/url"
"time"
"sigs.k8s.io/controller-runtime/pkg/client"
@ -152,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,
}
}

68
auth/options_test.go Normal file
View File

@ -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")
}
})
}
}

View File

@ -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.

View File

@ -13,6 +13,7 @@ require (
github.com/go-logr/logr v1.4.2
github.com/onsi/gomega v1.37.0
github.com/opencontainers/go-digest v1.0.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
@ -50,7 +51,6 @@ 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
go.yaml.in/yaml/v2 v2.4.2 // 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

View File

@ -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)
}
}
}

View File

@ -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",
},
},
},
},
},
},
{

View File

@ -19,6 +19,7 @@ package github
import (
"context"
"crypto/sha256"
"crypto/tls"
"fmt"
"net/http"
"net/url"
@ -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,6 +116,13 @@ func New(opts ...OptFunc) (*Client, error) {
return p, nil
}
// WithTLSConfig sets the tls config to use with the transport.
func WithTLSConfig(tlsConfig *tls.Config) OptFunc {
return func(p *Client) {
p.tlsConfig = tlsConfig
}
}
// WithAppData configures the client using data from a map
func WithAppData(appData map[string][]byte) OptFunc {
return func(p *Client) {

View File

@ -18,6 +18,8 @@ package github
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"net/http"
@ -257,3 +259,55 @@ func TestClient_GetCredentials(t *testing.T) {
})
}
}
func TestClient_TLS_RootCA(t *testing.T) {
g := NewWithT(t)
// spin up a TLS server with a self-signed cert
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
tok := &AppToken{
Token: "enterprise-token",
ExpiresAt: time.Now().Add(time.Hour),
}
_ = json.NewEncoder(w).Encode(tok)
})
srv := httptest.NewTLSServer(handler)
defer srv.Close()
// generate a dummy GitHub App keypair
kp, err := ssh.GenerateKeyPair(ssh.RSA_4096)
g.Expect(err).NotTo(HaveOccurred())
opts := []OptFunc{
WithAppData(map[string][]byte{
KeyAppID: []byte("123"),
KeyAppInstallationID: []byte("456"),
KeyAppPrivateKey: kp.PrivateKey,
KeyAppBaseURL: []byte(srv.URL),
}),
}
t.Run("it should error out if a Root CA is not provided", func(t *testing.T) {
g := NewWithT(t)
// with no TLSConfig, system roots wont trust our servers cert
_, _, err := GetCredentials(context.Background(), opts...)
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring("certificate signed by unknown authority"))
})
t.Run("it should succeed when Root CA is provided", func(t *testing.T) {
g := NewWithT(t)
// create a cert pool with server cert
certPool := x509.NewCertPool()
certPool.AddCert(srv.Certificate())
opts := append(opts,
WithTLSConfig(&tls.Config{RootCAs: certPool}),
)
user, pass, err := GetCredentials(context.Background(), opts...)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(user).To(Equal(AccessTokenUsername))
g.Expect(pass).To(Equal("enterprise-token"))
})
}

View File

@ -14,7 +14,7 @@ require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/elazarl/goproxy v1.7.2
github.com/fluxcd/gitkit v0.6.0
github.com/fluxcd/pkg/git v0.34.0
github.com/fluxcd/pkg/git v0.35.0
github.com/fluxcd/pkg/gittestserver v0.18.0
github.com/fluxcd/pkg/ssh v0.20.0
github.com/fluxcd/pkg/version v0.9.0

View File

@ -13,8 +13,8 @@ replace (
require (
github.com/fluxcd/go-git-providers v0.22.0
github.com/fluxcd/pkg/git v0.34.0
github.com/fluxcd/pkg/git/gogit v0.37.0
github.com/fluxcd/pkg/git v0.35.0
github.com/fluxcd/pkg/git/gogit v0.38.0
github.com/fluxcd/pkg/gittestserver v0.18.0
github.com/fluxcd/pkg/ssh v0.20.0
github.com/go-git/go-git/v5 v5.16.2

26
oci/fs.go Normal file
View File

@ -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 oci
import "github.com/fluxcd/pkg/oci/internal/fs"
// RenameWithFallback attempts to rename a file or directory, but falls back to
// copying in the event of a cross-device link error. If the fallback copy
// succeeds, src is still removed, emulating normal rename behavior.
func RenameWithFallback(src, dst string) error {
return fs.RenameWithFallback(src, dst)
}

View File

@ -18,11 +18,11 @@ require (
github.com/elazarl/goproxy v1.7.2
github.com/fluxcd/cli-utils v0.36.0-flux.14
github.com/fluxcd/pkg/apis/meta v1.18.0
github.com/fluxcd/pkg/auth v0.23.0
github.com/fluxcd/pkg/auth v0.24.0
github.com/fluxcd/pkg/cache v0.10.0
github.com/fluxcd/pkg/git v0.34.0
github.com/fluxcd/pkg/git/gogit v0.37.0
github.com/fluxcd/pkg/runtime v0.73.0
github.com/fluxcd/pkg/git v0.35.0
github.com/fluxcd/pkg/git/gogit v0.38.0
github.com/fluxcd/pkg/runtime v0.80.0
github.com/fluxcd/test-infra/tftestenv v0.0.0-20250626232827-e0ca9c3f8d7b
github.com/go-git/go-git/v5 v5.16.2
github.com/google/go-containerregistry v0.20.6

View File

@ -30,6 +30,16 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
)
// TLSConfigOption is a functional option for configuring TLS behavior.
type TLSConfigOption func(*tlsConfig)
// WithSystemCertPool enables the use of system certificate pool in addition to user-provided CA certificates.
func WithSystemCertPool() TLSConfigOption {
return func(c *tlsConfig) {
c.useSystemCertPool = true
}
}
// AuthMethodsFromSecret extracts all available authentication methods from a Kubernetes secret.
//
// The function attempts to parse all supported authentication methods from the secret data.
@ -39,12 +49,21 @@ import (
// Supported authentication methods:
// - Basic authentication (username/password)
// - Bearer token authentication
// - Token authentication
// - SSH authentication (private key, known hosts)
// - GitHub App authentication (app ID, installation ID, private key)
// - TLS client certificates
//
// Multiple authentication methods can be present in a single secret and will be extracted
// simultaneously, enabling use cases like Basic Auth + TLS or Bearer Token + TLS.
func AuthMethodsFromSecret(ctx context.Context, secret *corev1.Secret) (*AuthMethods, error) {
//
// Options can be provided to configure TLS extraction behavior.
func AuthMethodsFromSecret(ctx context.Context, secret *corev1.Secret, opts ...AuthMethodsOption) (*AuthMethods, error) {
config := &authMethodsConfig{}
for _, opt := range opts {
opt(config)
}
var methods AuthMethods
if err := trySetAuth(ctx, secret, &methods.Basic, BasicAuthFromSecret); err != nil {
@ -55,11 +74,21 @@ func AuthMethodsFromSecret(ctx context.Context, secret *corev1.Secret) (*AuthMet
return nil, err
}
if err := trySetAuth(ctx, secret, &methods.Token, TokenAuthFromSecret); err != nil {
return nil, err
}
if err := trySetAuth(ctx, secret, &methods.SSH, SSHAuthFromSecret); err != nil {
return nil, err
}
if err := trySetAuth(ctx, secret, &methods.TLS, TLSConfigFromSecret); err != nil {
if err := trySetAuth(ctx, secret, &methods.GitHubAppData, GitHubAppDataFromSecret); err != nil {
return nil, err
}
if err := trySetAuth(ctx, secret, &methods.TLS, func(ctx context.Context, secret *corev1.Secret) (*tls.Config, error) {
return TLSConfigFromSecret(ctx, secret, config.tlsConfigOpts...)
}); err != nil {
return nil, err
}
@ -73,7 +102,15 @@ func AuthMethodsFromSecret(ctx context.Context, secret *corev1.Secret) (*AuthMet
// (certFile, keyFile, caFile) as fallbacks, logging warnings when they are used.
//
// Standard field names always take precedence over legacy ones.
func TLSConfigFromSecret(ctx context.Context, secret *corev1.Secret) (*tls.Config, error) {
//
// Optional TLSConfigOption parameters can be used to configure CA certificate handling:
// - WithSystemCertPool(): Include system certificates in addition to user-provided CA
func TLSConfigFromSecret(ctx context.Context, secret *corev1.Secret, opts ...TLSConfigOption) (*tls.Config, error) {
config := &tlsConfig{}
for _, opt := range opts {
opt(config)
}
logger := log.FromContext(ctx)
certData, err := getTLSCertificateData(secret, logger)
if err != nil {
@ -87,7 +124,7 @@ func TLSConfigFromSecret(ctx context.Context, secret *corev1.Secret) (*tls.Confi
return nil, err
}
return buildTLSConfig(certData)
return buildTLSConfig(certData, config)
}
// ProxyURLFromSecret creates a proxy URL from a Kubernetes secret.
@ -167,15 +204,26 @@ func BasicAuthFromSecret(ctx context.Context, secret *corev1.Secret) (*BasicAuth
//
// The function expects the secret to contain "bearerToken" field.
// The field is required and the function will return an error if it is missing.
func BearerAuthFromSecret(ctx context.Context, secret *corev1.Secret) (*BearerAuth, error) {
func BearerAuthFromSecret(ctx context.Context, secret *corev1.Secret) (BearerAuth, error) {
tokenData, exists := secret.Data[KeyBearerToken]
if !exists {
return nil, &KeyNotFoundError{Key: KeyBearerToken, Secret: secret}
return "", &KeyNotFoundError{Key: KeyBearerToken, Secret: secret}
}
return &BearerAuth{
Token: string(tokenData),
}, nil
return BearerAuth(tokenData), nil
}
// TokenAuthFromSecret retrieves token authentication credentials from a Kubernetes secret.
//
// The function expects the secret to contain "token" field.
// The field is required and the function will return an error if it is missing.
func TokenAuthFromSecret(ctx context.Context, secret *corev1.Secret) (TokenAuth, error) {
tokenData, exists := secret.Data[KeyToken]
if !exists {
return "", &KeyNotFoundError{Key: KeyToken, Secret: secret}
}
return TokenAuth(tokenData), nil
}
// SSHAuthFromSecret retrieves SSH authentication credentials from a Kubernetes secret.
@ -210,12 +258,56 @@ func SSHAuthFromSecret(ctx context.Context, secret *corev1.Secret) (*SSHAuth, er
return auth, nil
}
// GitHubAppDataFromSecret retrieves GitHub App authentication data from a Kubernetes secret.
//
// The function expects the secret to contain "githubAppID", "githubAppInstallationID", and
// "githubAppPrivateKey" fields. All three fields are required and the function will return
// an error if any is missing. Optional "githubAppBaseURL" field can be present for GitHub
// Enterprise Server instances.
func GitHubAppDataFromSecret(ctx context.Context, secret *corev1.Secret) (GitHubAppData, error) {
_, hasAppID := secret.Data[KeyGitHubAppID]
_, hasInstallationID := secret.Data[KeyGitHubAppInstallationID]
_, hasPrivateKey := secret.Data[KeyGitHubAppPrivateKey]
// Complete absence - return KeyNotFoundError (will be ignored by trySetAuth)
if !hasAppID && !hasInstallationID && !hasPrivateKey {
return nil, &KeyNotFoundError{Key: KeyGitHubAppID, Secret: secret}
}
// Check for required fields - partial presence is an error
if !hasAppID {
return nil, &KeyNotFoundError{Key: KeyGitHubAppID, Secret: secret}
}
if !hasInstallationID {
return nil, &KeyNotFoundError{Key: KeyGitHubAppInstallationID, Secret: secret}
}
if !hasPrivateKey {
return nil, &KeyNotFoundError{Key: KeyGitHubAppPrivateKey, Secret: secret}
}
data := GitHubAppData{
KeyGitHubAppID: secret.Data[KeyGitHubAppID],
KeyGitHubAppInstallationID: secret.Data[KeyGitHubAppInstallationID],
KeyGitHubAppPrivateKey: secret.Data[KeyGitHubAppPrivateKey],
}
if baseURLData, exists := secret.Data[KeyGitHubAppBaseURL]; exists {
data[KeyGitHubAppBaseURL] = baseURLData
}
return data, nil
}
func getTLSCertificateData(secret *corev1.Secret, logger logr.Logger) (*tlsCertificateData, error) {
return newTLSCertificateData(secret, logger)
}
func buildTLSConfig(certData *tlsCertificateData) (*tls.Config, error) {
tlsConfig := &tls.Config{}
func buildTLSConfig(certData *tlsCertificateData, config *tlsConfig) (*tls.Config, error) {
tlsConfig := &tls.Config{
// Note: InsecureSkipVerify is explicitly set to false in accordance with Flux security policy.
// TLS certificates must be validated using CA certificates or the system trust store.
InsecureSkipVerify: false,
}
if certData.hasCertPair() {
cert, err := tls.X509KeyPair(certData.cert, certData.key)
@ -226,7 +318,17 @@ func buildTLSConfig(certData *tlsCertificateData) (*tls.Config, error) {
}
if certData.hasCA() {
caCertPool := x509.NewCertPool()
var caCertPool *x509.CertPool
if config.useSystemCertPool {
var err error
caCertPool, err = x509.SystemCertPool()
if err != nil {
return nil, fmt.Errorf("cannot retrieve system certificate pool: %w", err)
}
} else {
caCertPool = x509.NewCertPool()
}
if !caCertPool.AppendCertsFromPEM(certData.caCert) {
return nil, fmt.Errorf("failed to parse CA certificate")
}

View File

@ -38,21 +38,20 @@ func TestAuthMethodsFromSecret(t *testing.T) {
validCACert, _, _ := generateTestCertificates(t)
tests := []struct {
name string
secretData map[string][]byte
wantBasic bool
wantBearer bool
wantSSH bool
wantTLS bool
wantErr error
name string
secretData map[string][]byte
opt []secrets.AuthMethodsOption
wantBasic bool
wantBearer bool
wantToken bool
wantSSH bool
wantGitHubApp bool
wantTLS bool
wantErr error
}{
{
name: "empty secret",
secretData: map[string][]byte{},
wantBasic: false,
wantBearer: false,
wantSSH: false,
wantTLS: false,
},
{
name: "basic auth only",
@ -62,6 +61,7 @@ func TestAuthMethodsFromSecret(t *testing.T) {
},
wantBasic: true,
wantBearer: false,
wantToken: false,
wantSSH: false,
wantTLS: false,
},
@ -72,6 +72,7 @@ func TestAuthMethodsFromSecret(t *testing.T) {
},
wantBasic: false,
wantBearer: true,
wantToken: false,
wantSSH: false,
wantTLS: false,
},
@ -83,6 +84,7 @@ func TestAuthMethodsFromSecret(t *testing.T) {
},
wantBasic: false,
wantBearer: false,
wantToken: false,
wantSSH: true,
wantTLS: false,
},
@ -93,6 +95,7 @@ func TestAuthMethodsFromSecret(t *testing.T) {
},
wantBasic: false,
wantBearer: false,
wantToken: false,
wantSSH: false,
wantTLS: true,
},
@ -105,6 +108,7 @@ func TestAuthMethodsFromSecret(t *testing.T) {
},
wantBasic: true,
wantBearer: false,
wantToken: false,
wantSSH: false,
wantTLS: true,
},
@ -116,23 +120,74 @@ func TestAuthMethodsFromSecret(t *testing.T) {
},
wantBasic: false,
wantBearer: true,
wantToken: false,
wantSSH: false,
wantTLS: true,
},
{
name: "all authentication methods",
name: "token only",
secretData: map[string][]byte{
secrets.KeyUsername: []byte("testuser"),
secrets.KeyPassword: []byte("testpass"),
secrets.KeyBearerToken: []byte("token123"),
secrets.KeySSHPrivateKey: []byte(sshPrivateKey),
secrets.KeySSHKnownHosts: []byte(sshKnownHosts),
secrets.KeyCACert: validCACert,
secrets.KeyToken: []byte("api-token-123"),
},
wantBasic: false,
wantBearer: false,
wantToken: true,
wantSSH: false,
wantTLS: false,
},
{
name: "bearer token + basic auth",
secretData: map[string][]byte{
secrets.KeyUsername: []byte("testuser"),
secrets.KeyPassword: []byte("testpass"),
secrets.KeyBearerToken: []byte("token123"),
},
wantBasic: true,
wantBearer: true,
wantSSH: true,
wantTLS: true,
wantToken: false,
wantSSH: false,
wantTLS: false,
},
{
name: "all authentication methods",
secretData: map[string][]byte{
secrets.KeyUsername: []byte("testuser"),
secrets.KeyPassword: []byte("testpass"),
secrets.KeyBearerToken: []byte("token123"),
secrets.KeyToken: []byte("api-token-123"),
secrets.KeySSHPrivateKey: []byte(sshPrivateKey),
secrets.KeySSHKnownHosts: []byte(sshKnownHosts),
secrets.KeyGitHubAppID: []byte("123456"),
secrets.KeyGitHubAppInstallationID: []byte("7890123"),
secrets.KeyGitHubAppPrivateKey: []byte("test-private-key"),
secrets.KeyCACert: validCACert,
},
wantBasic: true,
wantBearer: true,
wantToken: true,
wantSSH: true,
wantGitHubApp: true,
wantTLS: true,
},
{
name: "GitHub App only",
secretData: map[string][]byte{
secrets.KeyGitHubAppID: []byte("123456"),
secrets.KeyGitHubAppInstallationID: []byte("7890123"),
secrets.KeyGitHubAppPrivateKey: []byte("test-private-key"),
},
wantGitHubApp: true,
},
{
name: "GitHub App + CA (source-controller PR scenario)",
secretData: map[string][]byte{
secrets.KeyGitHubAppID: []byte("123456"),
secrets.KeyGitHubAppInstallationID: []byte("7890123"),
secrets.KeyGitHubAppPrivateKey: []byte("test-private-key"),
secrets.KeyCACert: validCACert,
},
wantGitHubApp: true,
wantTLS: true,
},
{
name: "malformed SSH auth with valid TLS",
@ -167,6 +222,24 @@ func TestAuthMethodsFromSecret(t *testing.T) {
},
wantErr: fmt.Errorf("secret 'test-namespace/test-secret': malformed basic auth - has 'username' but missing 'password'"),
},
{
name: "TLS cert present, with all options",
secretData: map[string][]byte{
secrets.KeyCACert: validCACert,
},
opt: []secrets.AuthMethodsOption{secrets.WithTLSSystemCertPool()},
wantTLS: true,
},
{
name: "no TLS cert, with all options",
secretData: map[string][]byte{
secrets.KeyUsername: []byte("testuser"),
secrets.KeyPassword: []byte("testpass"),
},
opt: []secrets.AuthMethodsOption{secrets.WithTLSSystemCertPool()},
wantBasic: true,
wantTLS: false,
},
}
for _, tt := range tests {
@ -182,7 +255,7 @@ func TestAuthMethodsFromSecret(t *testing.T) {
Data: tt.secretData,
}
result, err := secrets.AuthMethodsFromSecret(ctx, secret)
result, err := secrets.AuthMethodsFromSecret(ctx, secret, tt.opt...)
if tt.wantErr != nil {
g.Expect(err).To(HaveOccurred())
@ -196,7 +269,9 @@ func TestAuthMethodsFromSecret(t *testing.T) {
g.Expect(result.HasBasicAuth()).To(Equal(tt.wantBasic))
g.Expect(result.HasBearerAuth()).To(Equal(tt.wantBearer))
g.Expect(result.HasTokenAuth()).To(Equal(tt.wantToken))
g.Expect(result.HasSSH()).To(Equal(tt.wantSSH))
g.Expect(result.HasGitHubAppData()).To(Equal(tt.wantGitHubApp))
g.Expect(result.HasTLS()).To(Equal(tt.wantTLS))
if tt.wantBasic {
@ -206,8 +281,13 @@ func TestAuthMethodsFromSecret(t *testing.T) {
}
if tt.wantBearer {
g.Expect(result.Bearer).ToNot(BeNil())
g.Expect(result.Bearer.Token).To(Equal("token123"))
g.Expect(result.Bearer).ToNot(BeEmpty())
g.Expect(string(result.Bearer)).To(Equal("token123"))
}
if tt.wantToken {
g.Expect(result.Token).ToNot(BeEmpty())
g.Expect(string(result.Token)).To(Equal("api-token-123"))
}
if tt.wantSSH {
@ -216,11 +296,143 @@ func TestAuthMethodsFromSecret(t *testing.T) {
g.Expect(result.SSH.KnownHosts).ToNot(BeEmpty())
}
if tt.wantTLS {
g.Expect(result.TLS).ToNot(BeNil())
g.Expect(result.TLS.RootCAs).ToNot(BeNil())
if tt.wantGitHubApp {
g.Expect(result.GitHubAppData).ToNot(BeNil())
g.Expect(result.GitHubAppData).To(HaveKey(secrets.KeyGitHubAppID))
g.Expect(result.GitHubAppData).To(HaveKey(secrets.KeyGitHubAppInstallationID))
g.Expect(result.GitHubAppData).To(HaveKey(secrets.KeyGitHubAppPrivateKey))
}
if tt.wantTLS {
g.Expect(result.TLS).ToNot(BeNil())
}
})
}
}
func TestGitHubAppDataFromSecret(t *testing.T) {
t.Parallel()
tests := []struct {
name string
secretData map[string][]byte
wantData map[string][]byte
errMsg string
}{
{
name: "valid GitHub App data with all fields",
secretData: map[string][]byte{
secrets.KeyGitHubAppID: []byte("123456"),
secrets.KeyGitHubAppInstallationID: []byte("7890123"),
secrets.KeyGitHubAppPrivateKey: []byte("test-private-key"),
secrets.KeyGitHubAppBaseURL: []byte("https://github.example.com"),
},
wantData: map[string][]byte{
secrets.KeyGitHubAppID: []byte("123456"),
secrets.KeyGitHubAppInstallationID: []byte("7890123"),
secrets.KeyGitHubAppPrivateKey: []byte("test-private-key"),
secrets.KeyGitHubAppBaseURL: []byte("https://github.example.com"),
},
},
{
name: "valid GitHub App data with required fields only",
secretData: map[string][]byte{
secrets.KeyGitHubAppID: []byte("123456"),
secrets.KeyGitHubAppInstallationID: []byte("7890123"),
secrets.KeyGitHubAppPrivateKey: []byte("test-private-key"),
},
wantData: map[string][]byte{
secrets.KeyGitHubAppID: []byte("123456"),
secrets.KeyGitHubAppInstallationID: []byte("7890123"),
secrets.KeyGitHubAppPrivateKey: []byte("test-private-key"),
},
},
{
name: "missing app ID",
secretData: map[string][]byte{
secrets.KeyGitHubAppInstallationID: []byte("7890123"),
secrets.KeyGitHubAppPrivateKey: []byte("test-private-key"),
},
errMsg: `secret 'default/github-app-secret': key 'githubAppID' not found`,
},
{
name: "missing installation ID",
secretData: map[string][]byte{
secrets.KeyGitHubAppID: []byte("123456"),
secrets.KeyGitHubAppPrivateKey: []byte("test-private-key"),
},
errMsg: `secret 'default/github-app-secret': key 'githubAppInstallationID' not found`,
},
{
name: "missing private key",
secretData: map[string][]byte{
secrets.KeyGitHubAppID: []byte("123456"),
secrets.KeyGitHubAppInstallationID: []byte("7890123"),
},
errMsg: `secret 'default/github-app-secret': key 'githubAppPrivateKey' not found`,
},
{
name: "completely empty secret",
secretData: map[string][]byte{},
errMsg: `secret 'default/github-app-secret': key 'githubAppID' not found`,
},
{
name: "partial GitHub App data - only app ID",
secretData: map[string][]byte{
secrets.KeyGitHubAppID: []byte("123456"),
},
errMsg: `secret 'default/github-app-secret': key 'githubAppInstallationID' not found`,
},
{
name: "partial GitHub App data - app ID and installation ID only",
secretData: map[string][]byte{
secrets.KeyGitHubAppID: []byte("123456"),
secrets.KeyGitHubAppInstallationID: []byte("7890123"),
},
errMsg: `secret 'default/github-app-secret': key 'githubAppPrivateKey' not found`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
g := NewWithT(t)
ctx := context.Background()
secret := testSecret(
withName("github-app-secret"),
withData(tt.secretData),
)
result, err := secrets.GitHubAppDataFromSecret(ctx, secret)
if tt.errMsg != "" {
g.Expect(err).To(MatchError(ContainSubstring(tt.errMsg)))
g.Expect(result).To(BeNil())
} else {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(result).ToNot(BeNil())
g.Expect(result).To(Equal(tt.wantData))
// Verify required fields are present
g.Expect(result).To(HaveKey(secrets.KeyGitHubAppID))
g.Expect(result).To(HaveKey(secrets.KeyGitHubAppInstallationID))
g.Expect(result).To(HaveKey(secrets.KeyGitHubAppPrivateKey))
// Verify content
g.Expect(string(result[secrets.KeyGitHubAppID])).To(Equal("123456"))
g.Expect(string(result[secrets.KeyGitHubAppInstallationID])).To(Equal("7890123"))
g.Expect(string(result[secrets.KeyGitHubAppPrivateKey])).To(Equal("test-private-key"))
// BaseURL should only be present if it was in the input
if tt.wantData[secrets.KeyGitHubAppBaseURL] != nil {
g.Expect(result).To(HaveKey(secrets.KeyGitHubAppBaseURL))
g.Expect(string(result[secrets.KeyGitHubAppBaseURL])).To(Equal("https://github.example.com"))
} else {
g.Expect(result).ToNot(HaveKey(secrets.KeyGitHubAppBaseURL))
}
}
})
}
}
@ -233,6 +445,7 @@ func TestTLSConfigFromSecret(t *testing.T) {
tests := []struct {
name string
secret *corev1.Secret
opts []secrets.TLSConfigOption
errMsg string
expectedFields map[string]string // legacy key -> preferred key mapping
}{
@ -361,6 +574,16 @@ func TestTLSConfigFromSecret(t *testing.T) {
),
errMsg: "secret 'default/tls-secret' must contain either 'ca.crt' or both 'tls.crt' and 'tls.key'",
},
{
name: "WithSystemCertPool option",
secret: testSecret(
withName("tls-secret"),
withData(map[string][]byte{
secrets.KeyCACert: caCert,
}),
),
opts: []secrets.TLSConfigOption{secrets.WithSystemCertPool()},
},
}
for _, tt := range tests {
@ -383,7 +606,7 @@ func TestTLSConfigFromSecret(t *testing.T) {
ctx = log.IntoContext(ctx, logr.Discard())
}
tlsConfig, err := secrets.TLSConfigFromSecret(ctx, tt.secret)
tlsConfig, err := secrets.TLSConfigFromSecret(ctx, tt.secret, tt.opts...)
if tt.errMsg != "" {
g.Expect(err).To(MatchError(ContainSubstring(tt.errMsg)))
@ -391,6 +614,12 @@ func TestTLSConfigFromSecret(t *testing.T) {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(tlsConfig).ToNot(BeNil())
// ServerName should be empty to allow automatic hostname verification
g.Expect(tlsConfig.ServerName).To(BeEmpty())
// InsecureSkipVerify must always be false per Flux security policy.
// The insecure parameter was removed to prevent bypassing certificate validation.
g.Expect(tlsConfig.InsecureSkipVerify).To(BeFalse())
hasCert := len(tt.secret.Data[secrets.KeyTLSCert]) > 0 || len(tt.secret.Data[secrets.LegacyKeyTLSCert]) > 0
hasKey := len(tt.secret.Data[secrets.KeyTLSPrivateKey]) > 0 || len(tt.secret.Data[secrets.LegacyKeyTLSPrivateKey]) > 0
hasCertPair := hasCert && hasKey
@ -702,7 +931,65 @@ func TestBearerAuthFromSecret(t *testing.T) {
g.Expect(err).To(MatchError(ContainSubstring(tt.errMsg)))
} else {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(bearerAuth.Token).To(Equal(tt.wantToken))
g.Expect(string(bearerAuth)).To(Equal(tt.wantToken))
}
})
}
}
func TestTokenAuthFromSecret(t *testing.T) {
t.Parallel()
tests := []struct {
name string
secret *corev1.Secret
wantToken string
errMsg string
}{
{
name: "valid token",
secret: testSecret(
withName("token-secret"),
withData(map[string][]byte{
secrets.KeyToken: []byte("api-token-123"),
}),
),
wantToken: "api-token-123",
},
{
name: "empty token",
secret: testSecret(
withName("token-secret"),
withData(map[string][]byte{
secrets.KeyToken: []byte(""),
}),
),
wantToken: "",
},
{
name: "missing token key",
secret: testSecret(
withName("token-secret"),
withData(map[string][]byte{}),
),
errMsg: `secret 'default/token-secret': key 'token' not found`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
g := NewWithT(t)
ctx := context.Background()
tokenAuth, err := secrets.TokenAuthFromSecret(ctx, tt.secret)
if tt.errMsg != "" {
g.Expect(err).To(MatchError(ContainSubstring(tt.errMsg)))
} else {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(string(tokenAuth)).To(Equal(tt.wantToken))
}
})
}

View File

@ -32,12 +32,15 @@ import (
//
// The function fetches the secret from the API server and then processes it using
// TLSConfigFromSecret. It supports the same field names and legacy field handling.
func TLSConfigFromSecretRef(ctx context.Context, c client.Client, secretRef types.NamespacedName) (*tls.Config, error) {
//
// Optional TLSConfigOption parameters can be used to configure CA certificate handling:
// - WithSystemCertPool(): Include system certificates in addition to user-provided CA
func TLSConfigFromSecretRef(ctx context.Context, c client.Client, secretRef types.NamespacedName, opts ...TLSConfigOption) (*tls.Config, error) {
secret, err := getSecret(ctx, c, secretRef)
if err != nil {
return nil, err
}
return TLSConfigFromSecret(ctx, secret)
return TLSConfigFromSecret(ctx, secret, opts...)
}
// ProxyURLFromSecretRef creates a proxy URL from a Kubernetes secret reference.
@ -52,6 +55,18 @@ func ProxyURLFromSecretRef(ctx context.Context, c client.Client, secretRef types
return ProxyURLFromSecret(ctx, secret)
}
// GitHubAppDataFromSecretRef retrieves GitHub App authentication data from a Kubernetes secret reference.
//
// The function fetches the secret from the API server and then processes it using
// GitHubAppDataFromSecret. It expects the same field structure for GitHub App configuration.
func GitHubAppDataFromSecretRef(ctx context.Context, c client.Client, secretRef types.NamespacedName) (GitHubAppData, error) {
secret, err := getSecret(ctx, c, secretRef)
if err != nil {
return nil, err
}
return GitHubAppDataFromSecret(ctx, secret)
}
// PullSecretsFromServiceAccountRef retrieves all image pull secrets referenced by a service account.
//
// The function resolves all secrets listed in the service account's imagePullSecrets field

View File

@ -41,6 +41,7 @@ func TestTLSConfigFromSecretRef(t *testing.T) {
name string
secretRef types.NamespacedName
secret *corev1.Secret // Secret to add to fake client (nil = not added)
opts []secrets.TLSConfigOption
errMsg string
}{
{
@ -60,6 +61,17 @@ func TestTLSConfigFromSecretRef(t *testing.T) {
secretRef: types.NamespacedName{Name: "missing-secret", Namespace: testNS},
errMsg: "secret 'default/missing-secret' not found",
},
{
name: "TLS secret with WithSystemCertPool option",
secretRef: types.NamespacedName{Name: "tls-secret", Namespace: testNS},
secret: testSecret(
withName("tls-secret"),
withData(map[string][]byte{
secrets.KeyCACert: caCert,
}),
),
opts: []secrets.TLSConfigOption{secrets.WithSystemCertPool()},
},
}
for _, tt := range tests {
@ -77,7 +89,7 @@ func TestTLSConfigFromSecretRef(t *testing.T) {
}
c := fakeClient(objects...)
tlsConfig, err := secrets.TLSConfigFromSecretRef(ctx, c, tt.secretRef)
tlsConfig, err := secrets.TLSConfigFromSecretRef(ctx, c, tt.secretRef, tt.opts...)
if tt.errMsg != "" {
g.Expect(err).To(MatchError(ContainSubstring(tt.errMsg)))
@ -85,6 +97,12 @@ func TestTLSConfigFromSecretRef(t *testing.T) {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(tlsConfig).ToNot(BeNil())
// ServerName should be empty to allow automatic hostname verification
g.Expect(tlsConfig.ServerName).To(BeEmpty())
// InsecureSkipVerify must always be false per Flux security policy.
// The insecure parameter was removed to prevent bypassing certificate validation.
g.Expect(tlsConfig.InsecureSkipVerify).To(BeFalse())
hasCert := len(tt.secret.Data[secrets.KeyTLSCert]) > 0 || len(tt.secret.Data[secrets.LegacyKeyTLSCert]) > 0
hasKey := len(tt.secret.Data[secrets.KeyTLSPrivateKey]) > 0 || len(tt.secret.Data[secrets.LegacyKeyTLSPrivateKey]) > 0
hasCertPair := hasCert && hasKey

View File

@ -73,10 +73,12 @@ const (
// AuthMethods holds all available authentication methods detected from a secret.
type AuthMethods struct {
Basic *BasicAuth
Bearer *BearerAuth
SSH *SSHAuth
TLS *tls.Config
Basic *BasicAuth
Bearer BearerAuth
Token TokenAuth
SSH *SSHAuth
GitHubAppData GitHubAppData
TLS *tls.Config
}
// HasBasicAuth returns true if basic authentication is available.
@ -86,7 +88,12 @@ func (am *AuthMethods) HasBasicAuth() bool {
// HasBearerAuth returns true if bearer token authentication is available.
func (am *AuthMethods) HasBearerAuth() bool {
return am.Bearer != nil
return am.Bearer != ""
}
// HasTokenAuth returns true if token authentication is available.
func (am *AuthMethods) HasTokenAuth() bool {
return am.Token != ""
}
// HasSSH returns true if SSH authentication is available.
@ -94,11 +101,36 @@ func (am *AuthMethods) HasSSH() bool {
return am.SSH != nil
}
// HasGitHubAppData returns true if GitHub App authentication data is available.
func (am *AuthMethods) HasGitHubAppData() bool {
return len(am.GitHubAppData) > 0
}
// HasTLS returns true if TLS configuration is available.
func (am *AuthMethods) HasTLS() bool {
return am.TLS != nil
}
// AuthMethodsOption configures the behavior of AuthMethodsFromSecret.
type AuthMethodsOption func(*authMethodsConfig)
// authMethodsConfig holds configuration for AuthMethods extraction.
type authMethodsConfig struct {
tlsConfigOpts []TLSConfigOption
}
// tlsConfig holds TLS-specific configuration options.
type tlsConfig struct {
useSystemCertPool bool
}
// WithTLSSystemCertPool enables the use of system certificate pool in addition to user-provided CA certificates.
func WithTLSSystemCertPool() AuthMethodsOption {
return func(cfg *authMethodsConfig) {
cfg.tlsConfigOpts = append(cfg.tlsConfigOpts, WithSystemCertPool())
}
}
// tlsCertificateData holds TLS certificate, key, and optional CA data
type tlsCertificateData struct {
cert []byte
@ -194,9 +226,13 @@ type BasicAuth struct {
}
// BearerAuth holds bearer token authentication credentials.
type BearerAuth struct {
Token string
}
type BearerAuth string
// TokenAuth holds generic token authentication credentials.
type TokenAuth string
// GitHubAppData holds GitHub App authentication data as key-value pairs.
type GitHubAppData = map[string][]byte
// SSHAuth holds SSH authentication credentials.
type SSHAuth struct {
@ -206,14 +242,6 @@ type SSHAuth struct {
Password string
}
// GitHubAppAuth holds GitHub App authentication credentials.
type GitHubAppAuth struct {
AppID string
InstallationID string
PrivateKey []byte
BaseURL string
}
// getSecretData retrieves data from secret with fallback support for legacy keys.
func getSecretData(secret *corev1.Secret, key, fallbackKey string, logger logr.Logger) []byte {
if data, exists := secret.Data[key]; exists {

View File

@ -767,6 +767,121 @@ func TestApply_IfNotPresent(t *testing.T) {
})
}
func TestApply_Cleanup_ExactMatch(t *testing.T) {
timeout := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
id := generateName("cleanup-exact")
objects, err := readManifest("testdata/test2.yaml", id)
if err != nil {
t.Fatal(err)
}
manager.SetOwnerLabels(objects, "app1", "default")
_, deployObject := getFirstObject(objects, "Deployment", id)
if err = normalize.UnstructuredList(objects); err != nil {
t.Fatal(err)
}
t.Run("creates objects as different managers", func(t *testing.T) {
// Apply all with prefix manager
for _, object := range objects {
obj := object.DeepCopy()
if err := manager.client.Patch(ctx, obj, client.Apply, client.FieldOwner("flux-apply-prefix")); err != nil {
t.Fatal(err)
}
}
// Apply deployment with exact match manager
deploy := deployObject.DeepCopy()
if err := manager.client.Patch(ctx, deploy, client.Apply, client.FieldOwner("flux")); err != nil {
t.Fatal(err)
}
// Check that the deployment has both managers
resultDeploy := deployObject.DeepCopy()
err = manager.Client().Get(ctx, client.ObjectKeyFromObject(deploy), resultDeploy)
if err != nil {
t.Fatal(err)
}
managedFields := resultDeploy.GetManagedFields()
foundExact := false
foundPrefix := false
for _, field := range managedFields {
if field.Manager == "flux" && field.Operation == metav1.ManagedFieldsOperationApply {
foundExact = true
}
if field.Manager == "flux-apply-prefix" && field.Operation == metav1.ManagedFieldsOperationApply {
foundPrefix = true
}
}
if !foundExact {
t.Errorf("Expected to find exact match manager 'flux' with Apply operation")
}
if !foundPrefix {
t.Errorf("Expected to find prefix manager 'flux-apply-prefix' with Apply operation")
}
})
t.Run("cleanup removes only exact match", func(t *testing.T) {
applyOpts := DefaultApplyOptions()
applyOpts.Cleanup = ApplyCleanupOptions{
FieldManagers: []FieldManager{
{
Name: "flux",
OperationType: metav1.ManagedFieldsOperationApply,
ExactMatch: true,
},
},
}
_, err := manager.ApplyAllStaged(ctx, objects, applyOpts)
if err != nil {
t.Fatal(err)
}
// Check that only exact match was removed
resultDeploy := deployObject.DeepCopy()
err = manager.Client().Get(ctx, client.ObjectKeyFromObject(resultDeploy), resultDeploy)
if err != nil {
t.Fatal(err)
}
managedFields := resultDeploy.GetManagedFields()
foundExact := false
foundPrefix := false
foundManager := false
for _, field := range managedFields {
t.Logf("Found managed field: Manager=%s, Operation=%s", field.Manager, field.Operation)
if field.Manager == "flux" {
foundExact = true
}
if field.Manager == "flux-apply-prefix" {
foundPrefix = true
}
if field.Manager == manager.owner.Field {
foundManager = true
}
}
if foundExact {
t.Errorf("Expected exact match 'flux' to be removed, but it was still present")
}
if !foundPrefix {
t.Errorf("Expected prefix match 'flux-apply-prefix' to remain, but it was not found")
}
if !foundManager {
t.Errorf("Expected manager '%s' to be present, but it was not found", manager.owner.Field)
}
})
}
func TestApply_Cleanup(t *testing.T) {
timeout := 10 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)

View File

@ -59,11 +59,27 @@ type FieldManager struct {
// Name is the name of the workflow managing fields.
Name string `json:"name"`
// ExactMatch controls the matching behavior for the manager name.
// When true, requires an exact match. When false, it uses prefix matching.
ExactMatch bool `json:"exactMatch"`
// OperationType is the type of operation performed by this manager, can be 'update' or 'apply'.
OperationType metav1.ManagedFieldsOperationType `json:"operationType"`
}
// PatchRemoveFieldsManagers returns a jsonPatch array for removing managers with matching prefix and operation type.
// matchFieldManager checks if the given ManagedFieldsEntry matches the specified FieldManager.
func matchFieldManager(entry metav1.ManagedFieldsEntry, manager FieldManager) bool {
if entry.Operation != manager.OperationType || entry.Subresource != "" {
return false
}
if manager.ExactMatch {
return entry.Manager == manager.Name
}
return strings.HasPrefix(entry.Manager, manager.Name)
}
// PatchRemoveFieldsManagers returns a jsonPatch array for removing managers with matching name or prefix.
func PatchRemoveFieldsManagers(object *unstructured.Unstructured, managers []FieldManager) []jsonPatch {
objEntries := object.GetManagedFields()
@ -72,9 +88,7 @@ func PatchRemoveFieldsManagers(object *unstructured.Unstructured, managers []Fie
for _, entry := range objEntries {
exclude := false
for _, manager := range managers {
if strings.HasPrefix(entry.Manager, manager.Name) &&
entry.Operation == manager.OperationType &&
entry.Subresource == "" {
if matchFieldManager(entry, manager) {
exclude = true
break
}
@ -95,8 +109,8 @@ func PatchRemoveFieldsManagers(object *unstructured.Unstructured, managers []Fie
return append(patches, newPatchReplace(managedFieldsPath, entries))
}
// PatchReplaceFieldsManagers returns a jsonPatch array for replacing the managers with matching prefix and operation type
// with the specified manager name and an apply operation.
// PatchReplaceFieldsManagers returns a jsonPatch array for replacing the managers with matching
// name and operation type with the specified manager name and an apply operation.
func PatchReplaceFieldsManagers(object *unstructured.Unstructured, managers []FieldManager, name string) ([]jsonPatch, error) {
objEntries := object.GetManagedFields()
@ -124,9 +138,7 @@ each_entry:
}
for _, manager := range managers {
if strings.HasPrefix(entry.Manager, manager.Name) &&
entry.Operation == manager.OperationType &&
entry.Subresource == "" {
if matchFieldManager(entry, manager) {
// if no previous managedField was found,
// rename the first match.