mirror of https://github.com/fluxcd/pkg.git
Compare commits
40 Commits
auth/v0.23
...
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 |
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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@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,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 won’t trust our server’s 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"))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
30
ssa/patch.go
30
ssa/patch.go
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue