Compare commits
100 Commits
rfc-creds
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da88a8e040 | ||
|
|
c8b4c4c620 | ||
|
|
c031d0c215 | ||
|
|
4f5b2fcab9 | ||
|
|
df3878d36a | ||
|
|
4e78a9d7e0 | ||
|
|
c1238ec834 | ||
|
|
99a7d2d735 | ||
|
|
19ab6eeb30 | ||
|
|
00d918ecaa | ||
|
|
474efa09cf | ||
|
|
5256361d8c | ||
|
|
c0938d351f | ||
|
|
2cee1d795e | ||
|
|
9a4b93056b | ||
|
|
8be056324a | ||
|
|
e45e46211b | ||
|
|
aa608bb769 | ||
|
|
7d27a26665 | ||
|
|
e9bcccfede | ||
|
|
d349ffe37d | ||
|
|
ac7f72b62b | ||
|
|
968bebadf6 | ||
|
|
2bfdadd301 | ||
|
|
36686b945c | ||
|
|
4e52adc7f0 | ||
|
|
21ca8d4d17 | ||
|
|
3e198177da | ||
|
|
7ba6dacc5c | ||
|
|
c97bdd412f | ||
|
|
4eaf59113f | ||
|
|
082a706f7f | ||
|
|
8668902dd1 | ||
|
|
8bc3ba3e1c | ||
|
|
2fdbde7fde | ||
|
|
7d7f20da25 | ||
|
|
e5128ea97e | ||
|
|
4f2374178c | ||
|
|
125464ed72 | ||
|
|
69e2c6bc7d | ||
|
|
7c9810ea3b | ||
|
|
c601a212f6 | ||
|
|
02734f28ba | ||
|
|
3d4eec61fe | ||
|
|
8a777bdd0f | ||
|
|
e2af45aee4 | ||
|
|
befe53a722 | ||
|
|
241d703e7f | ||
|
|
c432d380dd | ||
|
|
457abed9f9 | ||
|
|
5fc8afcaaf | ||
|
|
7bf0bda689 | ||
|
|
d9f51d047d | ||
|
|
dc5631f12b | ||
|
|
3f9d5bdc3d | ||
|
|
64e18014c3 | ||
|
|
e9226713e8 | ||
|
|
6a5e644798 | ||
|
|
0b0be7c1b6 | ||
|
|
484346ffcc | ||
|
|
5b3acbfcb5 | ||
|
|
2288dd90d6 | ||
|
|
af05357a62 | ||
|
|
64808a0eac | ||
|
|
2ead4fb31c | ||
|
|
b60dfbe970 | ||
|
|
ee8bb8d8a0 | ||
|
|
5f3098477e | ||
|
|
4c79a76e94 | ||
|
|
1516761fc8 | ||
|
|
52b1c1152b | ||
|
|
ab4bbffa5b | ||
|
|
e7314e8926 | ||
|
|
2666eaf8fc | ||
|
|
8262f8099e | ||
|
|
cbc5c736f4 | ||
|
|
ac71dd88a3 | ||
|
|
c5e5dfb8ae | ||
|
|
6ae880501e | ||
|
|
fd547dfe42 | ||
|
|
436dc7920a | ||
|
|
7a8cf63623 | ||
|
|
a6aefab55b | ||
|
|
5e5ee73046 | ||
|
|
8362c88791 | ||
|
|
340a048e8b | ||
|
|
4b2fc84402 | ||
|
|
2673348c2f | ||
|
|
7132eb3435 | ||
|
|
473b02ce5c | ||
|
|
862d9ddb6d | ||
|
|
33b9345883 | ||
|
|
e169a97577 | ||
|
|
4eddf80724 | ||
|
|
99f182be06 | ||
|
|
cf785cebcc | ||
|
|
7ff4c32d16 | ||
|
|
75bf2d608f | ||
|
|
f950198f9d | ||
|
|
2a2201fe56 |
6
.github/labels.yaml
vendored
6
.github/labels.yaml
vendored
@@ -44,12 +44,12 @@
|
|||||||
description: Feature request proposals in the RFC format
|
description: Feature request proposals in the RFC format
|
||||||
color: '#D621C3'
|
color: '#D621C3'
|
||||||
aliases: ['area/RFC']
|
aliases: ['area/RFC']
|
||||||
- name: backport:release/v2.5.x
|
|
||||||
description: To be backported to release/v2.5.x
|
|
||||||
color: '#ffd700'
|
|
||||||
- name: backport:release/v2.6.x
|
- name: backport:release/v2.6.x
|
||||||
description: To be backported to release/v2.6.x
|
description: To be backported to release/v2.6.x
|
||||||
color: '#ffd700'
|
color: '#ffd700'
|
||||||
- name: backport:release/v2.7.x
|
- name: backport:release/v2.7.x
|
||||||
description: To be backported to release/v2.7.x
|
description: To be backported to release/v2.7.x
|
||||||
color: '#ffd700'
|
color: '#ffd700'
|
||||||
|
- name: backport:release/v2.8.x
|
||||||
|
description: To be backported to release/v2.8.x
|
||||||
|
color: '#ffd700'
|
||||||
|
|||||||
2
.github/workflows/README.md
vendored
2
.github/workflows/README.md
vendored
@@ -23,7 +23,7 @@ amd when it finds a new controller version, the workflow performs the following
|
|||||||
- Updates the controller API package version in `go.mod`.
|
- Updates the controller API package version in `go.mod`.
|
||||||
- Patches the controller CRDs version in the `manifests/crds` overlay.
|
- Patches the controller CRDs version in the `manifests/crds` overlay.
|
||||||
- Patches the controller Deployment version in `manifests/bases` overlay.
|
- Patches the controller Deployment version in `manifests/bases` overlay.
|
||||||
- Opens a Pull Request against the `main` branch.
|
- Opens a Pull Request against the checked out branch.
|
||||||
- Triggers the e2e test suite to run for the opened PR.
|
- Triggers the e2e test suite to run for the opened PR.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/action.yaml
vendored
2
.github/workflows/action.yaml
vendored
@@ -24,6 +24,6 @@ jobs:
|
|||||||
name: action on ${{ matrix.version }}
|
name: action on ${{ matrix.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup flux
|
- name: Setup flux
|
||||||
uses: ./action
|
uses: ./action
|
||||||
|
|||||||
2
.github/workflows/backport.yaml
vendored
2
.github/workflows/backport.yaml
vendored
@@ -8,6 +8,6 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write # for reading and creating branches.
|
contents: write # for reading and creating branches.
|
||||||
pull-requests: write # for creating pull requests against release branches.
|
pull-requests: write # for creating pull requests against release branches.
|
||||||
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.4.0
|
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.9.0
|
||||||
secrets:
|
secrets:
|
||||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
|
|||||||
34
.github/workflows/conformance.yaml
vendored
34
.github/workflows/conformance.yaml
vendored
@@ -3,13 +3,13 @@ name: conformance
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ 'main', 'update-components', 'release/**', 'conform*' ]
|
branches: [ 'main', 'update-components-**', 'release/**', 'conform*' ]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.25.x
|
GO_VERSION: 1.26.x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
conform-kubernetes:
|
conform-kubernetes:
|
||||||
@@ -23,9 +23,9 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
make build
|
make build
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.30.0
|
||||||
cluster_name: ${{ steps.prep.outputs.CLUSTER }}
|
cluster_name: ${{ steps.prep.outputs.CLUSTER }}
|
||||||
@@ -76,13 +76,13 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
||||||
# Available versions can be found with "replicated cluster versions"
|
# Available versions can be found with "replicated cluster versions"
|
||||||
K3S_VERSION: [ 1.32.9, 1.33.5, 1.34.1 ]
|
K3S_VERSION: [ 1.33.7, 1.34.3, 1.35.0 ]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
||||||
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Create repository
|
- name: Create repository
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||||
- name: Create cluster
|
- name: Create cluster
|
||||||
id: create-cluster
|
id: create-cluster
|
||||||
uses: replicatedhq/replicated-actions/create-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
uses: replicatedhq/replicated-actions/create-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
kubernetes-distribution: "k3s"
|
kubernetes-distribution: "k3s"
|
||||||
@@ -150,7 +150,7 @@ jobs:
|
|||||||
kubectl delete ns flux-system --wait
|
kubectl delete ns flux-system --wait
|
||||||
- name: Delete cluster
|
- name: Delete cluster
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: replicatedhq/replicated-actions/remove-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
uses: replicatedhq/replicated-actions/remove-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
@@ -168,13 +168,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift
|
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift
|
||||||
OPENSHIFT_VERSION: [ 4.19.0-okd, 4.20.0-okd ]
|
OPENSHIFT_VERSION: [ 4.20.0-okd ]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
@@ -189,7 +189,7 @@ jobs:
|
|||||||
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
||||||
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Create repository
|
- name: Create repository
|
||||||
@@ -199,7 +199,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||||
- name: Create cluster
|
- name: Create cluster
|
||||||
id: create-cluster
|
id: create-cluster
|
||||||
uses: replicatedhq/replicated-actions/create-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
uses: replicatedhq/replicated-actions/create-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
kubernetes-distribution: "openshift"
|
kubernetes-distribution: "openshift"
|
||||||
@@ -240,7 +240,7 @@ jobs:
|
|||||||
kubectl delete ns flux-system --wait
|
kubectl delete ns flux-system --wait
|
||||||
- name: Delete cluster
|
- name: Delete cluster
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: replicatedhq/replicated-actions/remove-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
uses: replicatedhq/replicated-actions/remove-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
|
|||||||
10
.github/workflows/e2e-azure.yaml
vendored
10
.github/workflows/e2e-azure.yaml
vendored
@@ -29,14 +29,14 @@ jobs:
|
|||||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||||
steps:
|
steps:
|
||||||
- name: CheckoutD
|
- name: CheckoutD
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: tests/integration/go.sum
|
cache-dependency-path: tests/integration/go.sum
|
||||||
- name: Setup Terraform
|
- name: Setup Terraform
|
||||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
|
||||||
- name: Setup Flux CLI
|
- name: Setup Flux CLI
|
||||||
run: make build
|
run: make build
|
||||||
working-directory: ./
|
working-directory: ./
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SOPS_VER: 3.7.1
|
SOPS_VER: 3.7.1
|
||||||
- name: Authenticate to Azure
|
- name: Authenticate to Azure
|
||||||
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v1.4.6
|
uses: Azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v1.4.6
|
||||||
with:
|
with:
|
||||||
creds: '{"clientId":"${{ secrets.ARM_CLIENT_ID }}","clientSecret":"${{ secrets.ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.ARM_TENANT_ID }}"}'
|
creds: '{"clientId":"${{ secrets.ARM_CLIENT_ID }}","clientSecret":"${{ secrets.ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.ARM_TENANT_ID }}"}'
|
||||||
- name: Set dynamic variables in .env
|
- name: Set dynamic variables in .env
|
||||||
|
|||||||
16
.github/workflows/e2e-bootstrap.yaml
vendored
16
.github/workflows/e2e-bootstrap.yaml
vendored
@@ -17,27 +17,27 @@ jobs:
|
|||||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
**/go.sum
|
**/go.sum
|
||||||
**/go.mod
|
**/go.mod
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.30.0
|
||||||
cluster_name: kind
|
cluster_name: kind
|
||||||
# The versions below should target the newest Kubernetes version
|
# The versions below should target the newest Kubernetes version
|
||||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||||
node_image: ghcr.io/fluxcd/kindest/node:v1.32.1-amd64
|
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
||||||
kubectl_version: v1.32.0
|
kubectl_version: v1.33.0
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Setup yq
|
- name: Setup yq
|
||||||
uses: fluxcd/pkg/actions/yq@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/yq@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Set outputs
|
- name: Set outputs
|
||||||
|
|||||||
14
.github/workflows/e2e-gcp.yaml
vendored
14
.github/workflows/e2e-gcp.yaml
vendored
@@ -29,14 +29,14 @@ jobs:
|
|||||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: tests/integration/go.sum
|
cache-dependency-path: tests/integration/go.sum
|
||||||
- name: Setup Terraform
|
- name: Setup Terraform
|
||||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
|
||||||
- name: Setup Flux CLI
|
- name: Setup Flux CLI
|
||||||
run: make build
|
run: make build
|
||||||
working-directory: ./
|
working-directory: ./
|
||||||
@@ -56,11 +56,11 @@ jobs:
|
|||||||
- name: Setup gcloud
|
- name: Setup gcloud
|
||||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
|
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
- name: Log into us-central1-docker.pkg.dev
|
- name: Log into us-central1-docker.pkg.dev
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: us-central1-docker.pkg.dev
|
registry: us-central1-docker.pkg.dev
|
||||||
username: oauth2accesstoken
|
username: oauth2accesstoken
|
||||||
|
|||||||
14
.github/workflows/e2e.yaml
vendored
14
.github/workflows/e2e.yaml
vendored
@@ -23,16 +23,16 @@ jobs:
|
|||||||
- 5000:5000
|
- 5000:5000
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
**/go.sum
|
**/go.sum
|
||||||
**/go.mod
|
**/go.mod
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.30.0
|
||||||
cluster_name: kind
|
cluster_name: kind
|
||||||
@@ -40,13 +40,13 @@ jobs:
|
|||||||
config: .github/kind/config.yaml # disable KIND-net
|
config: .github/kind/config.yaml # disable KIND-net
|
||||||
# The versions below should target the oldest supported Kubernetes version
|
# The versions below should target the oldest supported Kubernetes version
|
||||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||||
node_image: ghcr.io/fluxcd/kindest/node:v1.32.1-amd64
|
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
||||||
kubectl_version: v1.32.0
|
kubectl_version: v1.33.0
|
||||||
- name: Setup Calico for network policy
|
- name: Setup Calico for network policy
|
||||||
run: |
|
run: |
|
||||||
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml
|
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
|
|||||||
6
.github/workflows/ossf.yaml
vendored
6
.github/workflows/ossf.yaml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Run analysis
|
- name: Run analysis
|
||||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
||||||
with:
|
with:
|
||||||
@@ -28,12 +28,12 @@ jobs:
|
|||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
publish_results: true
|
publish_results: true
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: SARIF file
|
name: SARIF file
|
||||||
path: results.sarif
|
path: results.sarif
|
||||||
retention-days: 5
|
retention-days: 5
|
||||||
- name: Upload SARIF results
|
- name: Upload SARIF results
|
||||||
uses: github/codeql-action/upload-sarif@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
34
.github/workflows/release.yaml
vendored
34
.github/workflows/release.yaml
vendored
@@ -22,35 +22,35 @@ jobs:
|
|||||||
packages: write # needed for ghcr access
|
packages: write # needed for ghcr access
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Unshallow
|
- name: Unshallow
|
||||||
run: git fetch --prune --unshallow
|
run: git fetch --prune --unshallow
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.x
|
go-version: 1.26.x
|
||||||
cache: false
|
cache: false
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
- name: Setup Syft
|
- name: Setup Syft
|
||||||
uses: anchore/sbom-action/download-syft@f8bdd1d8ac5e901a77a92f111440fdb1b593736b # v0.20.6
|
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||||
- name: Setup Cosign
|
- name: Setup Cosign
|
||||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||||
with:
|
with:
|
||||||
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
kustomize build manifests/crds > all-crds.yaml
|
kustomize build manifests/crds > all-crds.yaml
|
||||||
- name: Generate OpenAPI JSON schemas from CRDs
|
- name: Generate OpenAPI JSON schemas from CRDs
|
||||||
uses: fluxcd/pkg/actions/crdjsonschema@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/crdjsonschema@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
with:
|
with:
|
||||||
crd: all-crds.yaml
|
crd: all-crds.yaml
|
||||||
output: schemas
|
output: schemas
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
tar -czvf ./output/crd-schemas.tar.gz -C schemas .
|
tar -czvf ./output/crd-schemas.tar.gz -C schemas .
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
id: run-goreleaser
|
id: run-goreleaser
|
||||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7.2.1
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --skip=validate
|
args: release --skip=validate
|
||||||
@@ -103,9 +103,9 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@bf02f0a2d612cc07e0892166369fa8f63246aabb # main
|
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
||||||
- name: Setup Flux CLI
|
- name: Setup Flux CLI
|
||||||
uses: ./action/
|
uses: ./action/
|
||||||
with:
|
with:
|
||||||
@@ -116,13 +116,13 @@ jobs:
|
|||||||
VERSION=$(flux version --client | awk '{ print $NF }')
|
VERSION=$(flux version --client | awk '{ print $NF }')
|
||||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||||
@@ -150,7 +150,7 @@ jobs:
|
|||||||
--path="./flux-system" \
|
--path="./flux-system" \
|
||||||
--source=${{ github.repositoryUrl }} \
|
--source=${{ github.repositoryUrl }} \
|
||||||
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
|
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
|
||||||
- uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||||
with:
|
with:
|
||||||
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
||||||
- name: Sign manifests
|
- name: Sign manifests
|
||||||
|
|||||||
2
.github/workflows/scan.yaml
vendored
2
.github/workflows/scan.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read # for reading the repository code.
|
contents: read # for reading the repository code.
|
||||||
security-events: write # for uploading the CodeQL analysis results.
|
security-events: write # for uploading the CodeQL analysis results.
|
||||||
uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.4.0
|
uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.9.0
|
||||||
secrets:
|
secrets:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
fossa-token: ${{ secrets.FOSSA_TOKEN }}
|
fossa-token: ${{ secrets.FOSSA_TOKEN }}
|
||||||
|
|||||||
2
.github/workflows/sync-labels.yaml
vendored
2
.github/workflows/sync-labels.yaml
vendored
@@ -12,6 +12,6 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read # for reading the labels file.
|
contents: read # for reading the labels file.
|
||||||
issues: write # for creating and updating labels.
|
issues: write # for creating and updating labels.
|
||||||
uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.4.0
|
uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.9.0
|
||||||
secrets:
|
secrets:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
10
.github/workflows/update.yaml
vendored
10
.github/workflows/update.yaml
vendored
@@ -16,11 +16,11 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25.x
|
go-version: 1.26.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
**/go.sum
|
**/go.sum
|
||||||
**/go.mod
|
**/go.mod
|
||||||
@@ -96,7 +96,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
commit-message: |
|
commit-message: |
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
committer: GitHub <noreply@github.com>
|
committer: GitHub <noreply@github.com>
|
||||||
author: fluxcdbot <fluxcdbot@users.noreply.github.com>
|
author: fluxcdbot <fluxcdbot@users.noreply.github.com>
|
||||||
signoff: true
|
signoff: true
|
||||||
branch: update-components
|
branch: update-components-${{ github.ref_name }}
|
||||||
title: Update toolkit components
|
title: Update toolkit components
|
||||||
body: |
|
body: |
|
||||||
${{ steps.update.outputs.pr_body }}
|
${{ steps.update.outputs.pr_body }}
|
||||||
|
|||||||
13
.github/workflows/upgrade-fluxcd-pkg.yaml
vendored
Normal file
13
.github/workflows/upgrade-fluxcd-pkg.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
name: upgrade-fluxcd-pkg
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upgrade-fluxcd-pkg:
|
||||||
|
uses: fluxcd/gha-workflows/.github/workflows/upgrade-fluxcd-pkg.yaml@v0.9.0
|
||||||
|
secrets:
|
||||||
|
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
151
AGENTS.md
Normal file
151
AGENTS.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Guidance for AI coding assistants working in `fluxcd/flux2`. Read this file before making changes.
|
||||||
|
|
||||||
|
## Contribution workflow for AI agents
|
||||||
|
|
||||||
|
These rules come from [`fluxcd/flux2/CONTRIBUTING.md`](https://github.com/fluxcd/flux2/blob/main/CONTRIBUTING.md) and apply to every Flux repository.
|
||||||
|
|
||||||
|
- **Do not add `Signed-off-by` or `Co-authored-by` trailers with your agent name.** Only a human can legally certify the DCO.
|
||||||
|
- **Disclose AI assistance** with an `Assisted-by` trailer naming your agent and model:
|
||||||
|
```sh
|
||||||
|
git commit -s -m "Add support for X" --trailer "Assisted-by: <agent-name>/<model-id>"
|
||||||
|
```
|
||||||
|
The `-s` flag adds the human's `Signed-off-by` from their git config — do not remove it.
|
||||||
|
- **Commit message format:** Subject in imperative mood ("Add feature X" instead of "Adding feature X"), capitalized, no trailing period, ≤50 characters. Body wrapped at 72 columns, explaining what and why. No `@mentions` or `#123` issue references in the commit — put those in the PR description.
|
||||||
|
- **Trim verbiage:** in PR descriptions, commit messages, and code comments. No marketing prose, no restating the diff, no emojis.
|
||||||
|
- **Rebase, don't merge:** Never merge `main` into the feature branch; rebase onto the latest `main` and push with `--force-with-lease`. Squash before merge when asked.
|
||||||
|
- **Pre-PR gate:** `make tidy fmt vet && make test` must pass and the working tree must be clean.
|
||||||
|
- **Flux is GA:** Backward compatibility is mandatory. Breaking changes to CLI flags, output format, or behavior will be rejected. Design additive changes.
|
||||||
|
- **Copyright:** All new `.go` files must begin with the header from `cmd/flux/main.go` (Apache 2.0). Update the year to the current year when copying.
|
||||||
|
- **Tests:** New features, improvements and fixes must have test coverage. Add unit tests in `cmd/flux/*_test.go` tagged `//go:build unit`. Follow the existing `cmdTestCase` + golden file patterns. Run tests locally before pushing.
|
||||||
|
|
||||||
|
## Code quality
|
||||||
|
|
||||||
|
Before submitting code, review your changes for the following:
|
||||||
|
|
||||||
|
- **No secrets in logs or output.** Never surface auth tokens, passwords, deploy keys, or credential URLs in error messages, log lines, or CLI output. Bootstrap and source-secret commands handle sensitive material — take extra care.
|
||||||
|
- **No unchecked I/O.** Close HTTP response bodies, file handles, and tar readers in `defer` statements. Check and propagate errors from I/O operations.
|
||||||
|
- **No path traversal.** Validate and sanitize file paths extracted from archives or user input. Never `filepath.Join` with untrusted components without validation.
|
||||||
|
- **No command injection.** Do not shell out via `os/exec` for git, helm, or kustomize operations. Use the Go libraries already in use (`fluxcd/pkg/git`, `fluxcd/pkg/kustomize`, `fluxcd/pkg/ssa`).
|
||||||
|
- **No hardcoded defaults for security settings.** TLS verification must remain enabled by default. Git auth settings come from user-provided secrets.
|
||||||
|
- **Error handling.** Wrap errors with `%w` for chain inspection. Do not swallow errors silently. CLI errors must be actionable — tell the user what went wrong and how to fix it without leaking internal state.
|
||||||
|
- **Resource cleanup.** Ensure temporary files and directories (manifest staging, downloaded tarballs) are cleaned up on all code paths (success and error). Use `defer` and `t.TempDir()` in tests.
|
||||||
|
- **No panics.** Never use `panic` in runtime code paths. Return errors and let the CLI handle them gracefully.
|
||||||
|
- **Output discipline.** Machine-readable data (tables, YAML, JSON) goes to stdout via `rootCmd.OutOrStdout()`. Human-readable status messages go to stderr via the `stderrLogger`.
|
||||||
|
- **Minimal surface.** Keep new exported APIs in `pkg/` to the minimum needed. Every export is a backward-compatibility commitment.
|
||||||
|
|
||||||
|
## Project overview
|
||||||
|
|
||||||
|
flux2 is the Flux CLI (`flux` command) and distribution repository. It is **not** a controller — it consumes CRD APIs from six independent controller repos (source-controller, kustomize-controller, helm-controller, notification-controller, image-reflector-controller, image-automation-controller). It serves two purposes:
|
||||||
|
|
||||||
|
1. **CLI tool** — a Cobra-based binary that installs Flux onto Kubernetes clusters, bootstraps GitOps pipelines, and manages all Flux CRD objects (create, get, export, reconcile, suspend, resume, delete, diff, build, etc.).
|
||||||
|
2. **Distribution hub** — it bundles the Kustomize manifests for all Flux controllers and releases them as `manifests.tar.gz` on GitHub. Those manifests are also compiled into the binary itself via `//go:embed`.
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
- `cmd/flux/` — all CLI source. Single `main` package with one file per command or resource type. `main.go` defines the root cobra command with global flags. `manifests.embed.go` embeds the generated controller manifests via `//go:embed`.
|
||||||
|
- `internal/build/` — `flux build kustomization` logic (kustomize-based diff/build, SOPS secret masking).
|
||||||
|
- `internal/flags/` — custom `pflag.Value` types providing enum validation (e.g. `LogLevel`, `ECDSACurve`, `RSAKeyBits`, `PublicKeyAlgorithm`, `DecryptionProvider`).
|
||||||
|
- `internal/tree/` — tree-printing helper for `flux tree kustomization`.
|
||||||
|
- `internal/utils/` — shared helpers: `KubeClient`, `KubeConfig`, `NewScheme` (registers all controller API groups), `Apply` (SSA-based two-phase apply), `ExecKubectlCommand`, `ValidateComponents`.
|
||||||
|
- `pkg/bootstrap/` — bootstrap orchestration: `Run()`, `PlainGitBootstrapper`, `ProviderBootstrapper`. `provider/` has the git provider factory (GitHub, GitLab, Gitea, Bitbucket).
|
||||||
|
- `pkg/log/` — `Logger` interface (`Actionf`, `Generatef`, `Waitingf`, `Successf`, `Warningf`, `Failuref`).
|
||||||
|
- `pkg/manifestgen/` — manifest generation for install, sync, kustomization, and source secrets.
|
||||||
|
- `pkg/printers/` — specialized printers `TablePrinter` and `DyffPrinter`.
|
||||||
|
- `pkg/status/` — `StatusChecker` using `fluxcd/cli-utils` kstatus polling.
|
||||||
|
- `pkg/uninstall/` — `flux uninstall` logic.
|
||||||
|
- `manifests/` — Kustomize bases per controller, RBAC, network policies, CRD references, and `scripts/bundle.sh` which runs `kustomize build` to generate `cmd/flux/manifests/`.
|
||||||
|
- `tests/integration/` — cloud e2e tests (Azure/GCP) with their own `go.mod` and Terraform infrastructure.
|
||||||
|
- `rfcs/` — Request for Comments documents for major design proposals and changes.
|
||||||
|
|
||||||
|
## CLI architecture
|
||||||
|
|
||||||
|
Commands are Cobra-based, organized as parent + per-resource children:
|
||||||
|
- Group files (`create.go`, `get.go`, `reconcile.go`, etc.) register the parent subcommand.
|
||||||
|
- Per-resource files (`create_kustomization.go`, `get_helmrelease.go`, etc.) register children.
|
||||||
|
|
||||||
|
Core interfaces in `cmd/flux/` enable generic command implementations:
|
||||||
|
- `adapter` / `copyable` / `listAdapter` — wrap controller API types for generic CRUD.
|
||||||
|
- `reconcilable` — annotate-and-poll pattern for triggering reconciliation.
|
||||||
|
- `summarisable` — generic table output for `get` commands.
|
||||||
|
|
||||||
|
Each resource type (e.g. `kustomizationAdapter` in `kustomization.go`) wraps the controller API type and implements these interfaces. Follow this pattern when adding new resource support.
|
||||||
|
|
||||||
|
Commands interact with the Kubernetes API via `internal/utils.KubeClient()` → `client.WithWatch`. `internal/utils.NewScheme()` registers all six controller API groups plus core k8s types. `internal/utils.Apply()` implements SSA-based two-phase apply (CRDs/Namespaces first, then remaining objects).
|
||||||
|
|
||||||
|
## Manifest pipeline
|
||||||
|
|
||||||
|
1. `manifests/bases/<controller>/` contains a Kustomize base per controller referencing the controller's GitHub release for CRDs and deployment manifests.
|
||||||
|
2. `manifests/install/kustomization.yaml` assembles all bases plus RBAC and policies.
|
||||||
|
3. `manifests/scripts/bundle.sh` runs `kustomize build` on each base, writing output to `cmd/flux/manifests/` (not checked in — generated).
|
||||||
|
4. The Makefile `$(EMBEDDED_MANIFESTS_TARGET)` runs `bundle.sh` and creates a sentinel file `cmd/flux/.manifests.done`.
|
||||||
|
5. `cmd/flux/manifests.embed.go` uses `//go:embed manifests/*.yaml` to compile everything into the binary.
|
||||||
|
|
||||||
|
When modifying `manifests/`, always run `make build` and verify the generated output before committing. Never hand-edit files under `cmd/flux/manifests/`.
|
||||||
|
|
||||||
|
## Build, test, lint
|
||||||
|
|
||||||
|
All targets in the root `Makefile`. Go version tracks `go.mod`.
|
||||||
|
|
||||||
|
- `make tidy` — tidy the root module and `tests/integration/`.
|
||||||
|
- `make fmt` / `make vet` — run in the root module.
|
||||||
|
- `make build` — builds `bin/flux` (CGO disabled, version injected via ldflags). Depends on embedded manifests being generated.
|
||||||
|
- `make build-dev` — builds with `DEV_VERSION`.
|
||||||
|
- `make install` / `make install-dev` — `go install` or copy to `/usr/local/bin`.
|
||||||
|
- `make test` — unit tests with envtest: runs `tidy fmt vet install-envtest`, then `go test ./... -coverprofile cover.out --tags=unit $(TEST_ARGS)`.
|
||||||
|
- `make e2e` — e2e tests against a live cluster: `go test ./cmd/flux/... --tags=e2e -v -failfast`.
|
||||||
|
- `make test-with-kind` — sets up a kind cluster, runs e2e, tears it down.
|
||||||
|
- `make install-envtest` — downloads `setup-envtest` and fetches k8s binaries into `testbin/`.
|
||||||
|
|
||||||
|
Run a single test: `make test TEST_ARGS='-run TestCreate -v'`.
|
||||||
|
|
||||||
|
## Codegen and generated files
|
||||||
|
|
||||||
|
Check `go.mod` and the `Makefile` for current dependency and tool versions. The main codegen pipeline is the manifest bundle:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./manifests/scripts/bundle.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Generated files (never hand-edit):
|
||||||
|
|
||||||
|
- `cmd/flux/manifests/*.yaml` — generated by `bundle.sh` from `manifests/` sources.
|
||||||
|
- `cmd/flux/.manifests.done` — sentinel file tracking bundle state.
|
||||||
|
|
||||||
|
Bump `fluxcd/pkg/*` and controller `api` modules as a set. Run `make tidy` after any bump.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Standard `gofmt`. All exported names need doc comments.
|
||||||
|
- **Command pattern:** follow the existing group-parent + per-resource-child cobra structure. New resources need an adapter type implementing `adapter`, `copyable`, and the relevant command interfaces (`reconcilable`, `summarisable`, etc.).
|
||||||
|
- **Output:** stderr for human status messages via `stderrLogger` (Unicode symbols: `►` action, `✔` success, `✗` failure, `◎` waiting, `⚠️` warning, `✚` generate). Stdout for machine-readable data (tables, YAML, JSON) via `rootCmd.OutOrStdout()`.
|
||||||
|
- **Global flags:** kubeconfig flags come from `k8s.io/cli-runtime/pkg/genericclioptions.ConfigFlags`. Client tuning comes from `fluxcd/pkg/runtime/client.Options`. `FLUX_SYSTEM_NAMESPACE` env var overrides the default namespace.
|
||||||
|
- **SSA apply:** always use `internal/utils.Apply()` (two-phase: CRDs/Namespaces first, then rest). Do not apply manifests directly via the k8s client.
|
||||||
|
- **Reconcile triggering:** patch `meta.ReconcileRequestAnnotation` with a timestamp, then poll with `kstatus.Compute()` until ready. See `reconcile.go`.
|
||||||
|
- **Error handling:** return errors from `RunE`. Use `*RequestError` with exit codes for actionable CLI errors. Exit code 1 = warning, anything else = failure.
|
||||||
|
- **Flags:** use `internal/flags/` custom `pflag.Value` types for enum flags (providers, algorithms, sources). Add new enum types there.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Three test suites with build tags:
|
||||||
|
|
||||||
|
- **Unit** (`//go:build unit`): lives in `cmd/flux/*_test.go`. Uses `controller-runtime/envtest` for an in-process fake k8s API. CRDs are loaded from `cmd/flux/manifests/` (embedded manifests). Pattern: `cmdTestCase{args: "...", assert: assertGoldenFile("testdata/...")}`. The `executeCommand()` helper captures stdout.
|
||||||
|
- **E2e** (`//go:build e2e`): lives in `cmd/flux/*_test.go`. Requires a live cluster via `TEST_KUBECONFIG`. `TestMain` runs `flux install` for setup and teardown.
|
||||||
|
- **Integration** (`//go:build integration`): lives in `tests/integration/` with its own `go.mod`. Uses Terraform-provisioned cloud clusters.
|
||||||
|
|
||||||
|
Golden files live in `cmd/flux/testdata/`. Update them with `go test ./cmd/flux/... --tags=unit -update`.
|
||||||
|
|
||||||
|
Run a single unit test: `make test TEST_ARGS='-run TestInstall -v'`.
|
||||||
|
|
||||||
|
## Gotchas and non-obvious rules
|
||||||
|
|
||||||
|
- The `cmd/flux/manifests/` directory is **generated, not checked in**. It is created by `manifests/scripts/bundle.sh` and embedded into the binary. `make build` and `make test` both trigger the bundle if the sentinel file is stale.
|
||||||
|
- `kustomize` must be on `PATH` for `bundle.sh` to work. If you see "command not found" errors during build, install kustomize.
|
||||||
|
- `internal/utils.NewScheme()` registers all six controller API groups. Adding support for a new CRD type means updating the scheme registration there.
|
||||||
|
- The `VERSION` constant is injected via `-ldflags` at build time. In dev builds it defaults to `0.0.0-dev.0`. The embedded manifest version check (`isEmbeddedVersion`) determines whether `flux install` uses compiled-in manifests or downloads from GitHub.
|
||||||
|
- `resetCmdArgs()` in tests is critical — Cobra persists flag state between test runs. Every test case must reset to avoid pollution.
|
||||||
|
- `executeCommand()` captures stdout only. Stderr output (from `stderrLogger`) is not captured in test assertions. If your command's output goes to the wrong stream, tests will silently pass with empty golden files.
|
||||||
|
- The `adapter` / `listAdapter` interfaces use type assertions internally. If you add a new resource type and forget to implement an interface method, you'll get a runtime panic in the generic command handler, not a compile error. Add interface compliance checks (`var _ reconcilable = ...`).
|
||||||
|
- Bootstrap commands create real Git commits and push to real repos. E2e tests for bootstrap need careful cleanup. Do not add bootstrap e2e tests without a corresponding teardown.
|
||||||
|
- `pkg/` packages are importable by external consumers (e.g. Terraform provider, other tools). Treat their exported surface as public API.
|
||||||
235
CONTRIBUTING.md
235
CONTRIBUTING.md
@@ -1,154 +1,129 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Flux is [Apache 2.0 licensed](https://github.com/fluxcd/flux2/blob/main/LICENSE) and
|
Flux is [Apache 2.0 licensed](https://github.com/fluxcd/flux2/blob/main/LICENSE) and accepts contributions via GitHub pull requests.
|
||||||
accepts contributions via GitHub pull requests. This document outlines
|
This document outlines the conventions to get your contribution accepted.
|
||||||
some of the conventions on to make it easier to get your contribution
|
We gratefully welcome improvements to documentation as well as code contributions.
|
||||||
accepted.
|
|
||||||
|
|
||||||
We gratefully welcome improvements to issues and documentation as well as to
|
If you are new to the project, we recommend starting with documentation improvements or
|
||||||
code.
|
small bug fixes to get familiar with the codebase and the contribution process.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
The Flux project consists of a set of Kubernetes controllers and tools that implement the GitOps pattern.
|
||||||
|
The main repositories in the Flux project are:
|
||||||
|
|
||||||
|
- [fluxcd/flux2](https://github.com/fluxcd/flux2): The Flux distribution and command-line interface (CLI)
|
||||||
|
- [fluxcd/pkg](https://github.com/fluxcd/pkg): The GitOps Toolkit Go SDK for building Flux controllers and CLI plugins
|
||||||
|
- [fluxcd/source-controller](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git, OCI and Helm repositories, S3-compatible Buckets)
|
||||||
|
- [fluxcd/source-watcher](https://github.com/fluxcd/source-watcher): Kubernetes operator for advanced source composition and decomposition patterns
|
||||||
|
- [fluxcd/kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize
|
||||||
|
- [fluxcd/helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for lifecycle management of Helm releases
|
||||||
|
- [fluxcd/notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events (alerts and webhook receivers)
|
||||||
|
- [fluxcd/image-reflector-controller](https://github.com/fluxcd/image-reflector-controller): Kubernetes operator for scanning container registries for new image tags and digests
|
||||||
|
- [fluxcd/image-automation-controller](https://github.com/fluxcd/image-automation-controller): Kubernetes operator for patching container image tags and digests in Git repositories
|
||||||
|
- [fluxcd/website](https://github.com/fluxcd/website): The Flux documentation website accessible at <https://fluxcd.io/>
|
||||||
|
|
||||||
|
## AI Coding Assistants Guidance
|
||||||
|
|
||||||
|
Using AI Agents to help write your PR is acceptable, but as the author, you are responsible
|
||||||
|
for understanding the code and the documentation you submit. Please review all the AI-generated
|
||||||
|
content and make sure it follows the guidelines in this document before submitting your PR.
|
||||||
|
|
||||||
|
All Flux repositories contain an `AGENTS.md` file. You must point your AI Agent to
|
||||||
|
`AGENTS.md` and ask it to follow the guidelines and conventions described there.
|
||||||
|
|
||||||
|
Trim down the verbiage in the PR description, commit messages and code comments.
|
||||||
|
When engaging with Flux maintainers please refrain from using AI Agents to
|
||||||
|
generate responses, we want to talk to you, not to your AI Agent.
|
||||||
|
|
||||||
|
AI Agents **must not** add `Signed-off-by` or `Co-authored-by` tags to the commit message.
|
||||||
|
Only humans can legally certify the Developer Certificate of Origin ([DCO](https://developercertificate.org/)).
|
||||||
|
|
||||||
|
You should disclose the use of AI Agents in the description of your PR and
|
||||||
|
in the commit message using the `Assisted-by: AGENT_NAME/LLM_VERSION` tag.
|
||||||
|
|
||||||
|
Adding the `Assisted-by` tag to the commit message can be done with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git commit -s -m "Your commit message" --trailer "Assisted-by: <agent>/<model>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note** that the `Signed-off-by` tag is set via the `-s` flag using your real name and email
|
||||||
|
(`user.name` and `user.email` must be set in Git config).
|
||||||
|
|
||||||
|
Example of a commit message disclosing the use of AI assistance:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Add version info to plugin listing
|
||||||
|
|
||||||
|
Add a version column to the `flux plugin list` table output and populate
|
||||||
|
it with the semantic version info extracted from the plugin's recipe file.
|
||||||
|
For plugins installed via symlinks, the version is set to `unknown`.
|
||||||
|
|
||||||
|
Signed-off-by: Jane Doe <jane.doe@example.com>
|
||||||
|
Assisted-by: copilot/gpt-5.4
|
||||||
|
```
|
||||||
|
|
||||||
## Certificate of Origin
|
## Certificate of Origin
|
||||||
|
|
||||||
By contributing to this project you agree to the Developer Certificate of
|
By contributing to this project you agree to the Developer Certificate of Origin (DCO).
|
||||||
Origin (DCO). This document was created by the Linux Kernel community and is a
|
This document was created by the Linux Kernel community and is a simple statement that you,
|
||||||
simple statement that you, as a contributor, have the legal right to make the
|
as a contributor, have the legal right to make the contribution.
|
||||||
contribution.
|
|
||||||
|
|
||||||
We require all commits to be signed. By signing off with your signature, you
|
We require all commits to be signed. By signing off with your signature, you certify that you wrote
|
||||||
certify that you wrote the patch or otherwise have the right to contribute the
|
the patch or otherwise have the right to contribute the material by the rules of the [DCO](https://raw.githubusercontent.com/fluxcd/flux2/refs/heads/main/DCO):
|
||||||
material by the rules of the [DCO](DCO):
|
|
||||||
|
|
||||||
`Signed-off-by: Jane Doe <jane.doe@example.com>`
|
`Signed-off-by: Jane Doe <jane.doe@example.com>`
|
||||||
|
|
||||||
The signature must contain your real name
|
The signature must contain your real name (sorry, no pseudonyms or anonymous contributions).
|
||||||
(sorry, no pseudonyms or anonymous contributions)
|
If your `user.name` and `user.email` are set in your Git config,
|
||||||
If your `user.name` and `user.email` are configured in your Git config,
|
|
||||||
you can sign your commit automatically with `git commit -s`.
|
you can sign your commit automatically with `git commit -s`.
|
||||||
|
|
||||||
## Communications
|
|
||||||
|
|
||||||
For realtime communications we use Slack: To join the conversation, simply
|
|
||||||
join the [CNCF](https://slack.cncf.io/) Slack workspace and use the
|
|
||||||
[#flux-contributors](https://cloud-native.slack.com/messages/flux-contributors/) channel.
|
|
||||||
|
|
||||||
To discuss ideas and specifications we use [Github
|
|
||||||
Discussions](https://github.com/fluxcd/flux2/discussions).
|
|
||||||
|
|
||||||
For announcements we use a mailing list as well. Simply subscribe to
|
|
||||||
[flux-dev on cncf.io](https://lists.cncf.io/g/cncf-flux-dev)
|
|
||||||
to join the conversation (there you can also add calendar invites
|
|
||||||
to your Google calendar for our [Flux
|
|
||||||
meeting](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view)).
|
|
||||||
|
|
||||||
## Understanding Flux and the GitOps Toolkit
|
|
||||||
|
|
||||||
If you are entirely new to Flux and the GitOps Toolkit,
|
|
||||||
you might want to take a look at the [introductory talk and demo](https://www.youtube.com/watch?v=qQBtSkgl7tI).
|
|
||||||
|
|
||||||
This project is composed of:
|
|
||||||
|
|
||||||
- [flux2](https://github.com/fluxcd/flux2): The Flux CLI
|
|
||||||
- [source-controller](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git, OCI and Helm repositories, S3-compatible Buckets)
|
|
||||||
- [source-watcher](https://github.com/fluxcd/source-watcher): Kubernetes operator for advanced source composition and decomposition patterns
|
|
||||||
- [kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize
|
|
||||||
- [helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for building GitOps pipelines with Helm
|
|
||||||
- [notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events
|
|
||||||
- [image-reflector-controller](https://github.com/fluxcd/image-reflector-controller): Kubernetes operator for scanning container registries
|
|
||||||
- [image-automation-controller](https://github.com/fluxcd/image-automation-controller): Kubernetes operator for patches container image tags in Git
|
|
||||||
|
|
||||||
### Understanding the code
|
|
||||||
|
|
||||||
To get started with developing controllers, you might want to review
|
|
||||||
[our guide](https://fluxcd.io/flux/gitops-toolkit/source-watcher/) which
|
|
||||||
walks you through writing a short and concise controller that watches out
|
|
||||||
for source changes.
|
|
||||||
|
|
||||||
## How to run the test suite
|
|
||||||
|
|
||||||
Prerequisites:
|
|
||||||
|
|
||||||
* go >= 1.25
|
|
||||||
* kubectl >= 1.30
|
|
||||||
* kustomize >= 5.0
|
|
||||||
|
|
||||||
Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make install-envtest
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can run the unit tests with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make test
|
|
||||||
```
|
|
||||||
|
|
||||||
After [installing Kubernetes kind](https://kind.sigs.k8s.io/docs/user/quick-start#installation) on your machine,
|
|
||||||
create a cluster for testing with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make setup-kind
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can run the end-to-end tests with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
When the output of the Flux CLI changes, to automatically update the golden
|
|
||||||
files used in the test, pass `-update` flag to the test as:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make e2e TEST_ARGS="-update"
|
|
||||||
```
|
|
||||||
|
|
||||||
Since not all packages use golden files for testing, `-update` argument must be
|
|
||||||
passed only for the packages that use golden files. Use the variables
|
|
||||||
`TEST_PKG_PATH` for unit tests and `E2E_TEST_PKG_PATH` for e2e tests, to set the
|
|
||||||
path of the target test package:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Unit test
|
|
||||||
make test TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
|
|
||||||
# e2e test
|
|
||||||
make e2e E2E_TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
|
|
||||||
```
|
|
||||||
|
|
||||||
Teardown the e2e environment with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make cleanup-kind
|
|
||||||
```
|
|
||||||
|
|
||||||
## Acceptance policy
|
## Acceptance policy
|
||||||
|
|
||||||
These things will make a PR more likely to be accepted:
|
These things will make a PR more likely to be accepted:
|
||||||
|
|
||||||
- a well-described requirement
|
- Addressing an open issue, if one doesn't exist, please open an issue to discuss the problem and the proposed solution before submitting a PR.
|
||||||
- tests for new code
|
- Flux is GA software and we are committed to maintaining backward compatibility. If your contribution introduces a breaking change, expect for your PR to be rejected.
|
||||||
- tests for old code!
|
- New code and tests must follow the conventions in the existing code and tests. All new code must have good test coverage and be well documented.
|
||||||
- new code and tests follow the conventions in old code and tests
|
- All top-level Go code and exported names should have doc comments, as should non-trivial unexported type or function declarations.
|
||||||
- a good commit message (see below)
|
- Before submitting a PR, make sure that your code is properly formatted by running `make tidy fmt vet` and that all tests are passing by running `make test`.
|
||||||
- all code must abide [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
|
|
||||||
- names should abide [What's in a name](https://talks.golang.org/2014/names.slide#1)
|
|
||||||
- code must build on both Linux and Darwin, via plain `go build`
|
|
||||||
- code should have appropriate test coverage and tests should be written
|
|
||||||
to work with `go test`
|
|
||||||
|
|
||||||
In general, we will merge a PR once one maintainer has endorsed it.
|
In general, we will merge a PR once one maintainer has endorsed it.
|
||||||
For substantial changes, more people may become involved, and you might
|
For substantial changes, more people may become involved, and you might
|
||||||
get asked to resubmit the PR or divide the changes into more than one PR.
|
get asked to resubmit the PR or divide the changes into more than one PR.
|
||||||
|
|
||||||
### Format of the Commit Message
|
## Format of the Commit Message
|
||||||
|
|
||||||
For the GitOps Toolkit controllers we prefer the following rules for good commit messages:
|
For the Flux project we prefer the following rules:
|
||||||
|
|
||||||
- Limit the subject to 50 characters and write as the continuation
|
- Limit the subject to 50 characters, start with a capital letter and do not end with a period.
|
||||||
of the sentence "If applied, this commit will ..."
|
- Explain what and why in the body, if more than a trivial change; wrap it at 72 characters.
|
||||||
- Explain what and why in the body, if more than a trivial change;
|
- Use the imperative mood in the subject line (e.g., "Add support for X" instead of "Added support for X" or "Adds support for X").
|
||||||
wrap it at 72 characters.
|
- Do not include GitHub mentions to issues in the commit message, use the PR description instead (e.g., "Fixes #123" or "Closes #123").
|
||||||
|
- Do not include GitHub mentions to accounts (e.g., `@username` or `@team`) within the commit message.
|
||||||
|
|
||||||
The [following article](https://chris.beams.io/posts/git-commit/#seven-rules)
|
## Pull Request Process
|
||||||
has some more helpful advice on documenting your work.
|
|
||||||
|
Fork the repository and create a new branch for your changes, do not commit directly to the `main` branch.
|
||||||
|
Once you have made your changes and committed them, push your branch to your fork and open a pull request
|
||||||
|
against the `main` branch of the Flux repository.
|
||||||
|
|
||||||
|
During the review process, you may be asked to make changes to your PR. Add commits to address the feedback
|
||||||
|
without force pushing, as this will make it easier for reviewers to see the changes.
|
||||||
|
Before committing, make sure to run `make test` to ensure that your code will pass the CI checks.
|
||||||
|
|
||||||
|
When the review process is complete, you will be asked to **squash** the commits and **rebase** your branch.
|
||||||
|
**Do not merge** the `main` branch into your branch, instead, rebase your branch on top of the latest `main`
|
||||||
|
branch after **syncing your fork** with the latest changes from the Flux repository. After rebasing,
|
||||||
|
you can push your branch with the `--force-with-lease` option to update the PR.
|
||||||
|
|
||||||
|
## Communications
|
||||||
|
|
||||||
|
For realtime communications we use Slack. To reach out to the Flux maintainers and contributors,
|
||||||
|
join the [CNCF](https://slack.cncf.io/) Slack workspace and use the [#flux-contributors](https://cloud-native.slack.com/messages/flux-contributors/) channel.
|
||||||
|
To discuss ideas and specifications we use [GitHub Discussions](https://github.com/fluxcd/flux2/discussions).
|
||||||
|
|
||||||
|
For announcements, we use a mailing list as well. Subscribe to
|
||||||
|
[flux-dev on cncf.io](https://lists.cncf.io/g/cncf-flux-dev), there you can also add calendar invites
|
||||||
|
to your Google calendar for our [Flux dev meeting](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view).
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.22 AS builder
|
FROM alpine:3.23 AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates curl
|
RUN apk add --no-cache ca-certificates curl
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ RUN curl -sL https://dl.k8s.io/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \
|
|||||||
|
|
||||||
RUN kubectl version --client=true
|
RUN kubectl version --client=true
|
||||||
|
|
||||||
FROM alpine:3.22 AS flux-cli
|
FROM alpine:3.23 AS flux-cli
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -17,8 +17,8 @@ rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2
|
|||||||
all: test build
|
all: test build
|
||||||
|
|
||||||
tidy:
|
tidy:
|
||||||
go mod tidy -compat=1.25
|
go mod tidy -compat=1.26
|
||||||
cd tests/integration && go mod tidy -compat=1.25
|
cd tests/integration && go mod tidy -compat=1.26
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -51,6 +52,7 @@ type buildArtifactFlags struct {
|
|||||||
output string
|
output string
|
||||||
path string
|
path string
|
||||||
ignorePaths []string
|
ignorePaths []string
|
||||||
|
resolveSymlinks bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...)
|
var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...)
|
||||||
@@ -61,6 +63,7 @@ func init() {
|
|||||||
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.")
|
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.")
|
||||||
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.")
|
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.")
|
||||||
buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format")
|
buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format")
|
||||||
|
buildArtifactCmd.Flags().BoolVar(&buildArtifactArgs.resolveSymlinks, "resolve-symlinks", false, "resolve symlinks by copying their targets into the artifact")
|
||||||
|
|
||||||
buildCmd.AddCommand(buildArtifactCmd)
|
buildCmd.AddCommand(buildArtifactCmd)
|
||||||
}
|
}
|
||||||
@@ -85,6 +88,15 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path)
|
return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if buildArtifactArgs.resolveSymlinks {
|
||||||
|
resolved, cleanupDir, err := resolveSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolving symlinks failed: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(cleanupDir)
|
||||||
|
path = resolved
|
||||||
|
}
|
||||||
|
|
||||||
logger.Actionf("building artifact from %s", path)
|
logger.Actionf("building artifact from %s", path)
|
||||||
|
|
||||||
ociClient := oci.NewClient(oci.DefaultOptions())
|
ociClient := oci.NewClient(oci.DefaultOptions())
|
||||||
@@ -96,6 +108,141 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveSymlinks creates a temporary directory with symlinks resolved to their
|
||||||
|
// real file contents. This allows building artifacts from symlink trees (e.g.,
|
||||||
|
// those created by Nix) where the actual files live outside the source directory.
|
||||||
|
// It returns the resolved path and the temporary directory path for cleanup.
|
||||||
|
func resolveSymlinks(srcPath string) (string, string, error) {
|
||||||
|
absPath, err := filepath.Abs(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// For a single file, resolve the symlink and return the path to the
|
||||||
|
// copied file within the temp dir, preserving file semantics for callers.
|
||||||
|
if !info.IsDir() {
|
||||||
|
resolved, err := filepath.EvalSymlinks(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("resolving symlink for %s: %w", absPath, err)
|
||||||
|
}
|
||||||
|
tmpDir, err := os.MkdirTemp("", "flux-artifact-*")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
dst := filepath.Join(tmpDir, filepath.Base(absPath))
|
||||||
|
if err := copyFile(resolved, dst); err != nil {
|
||||||
|
os.RemoveAll(tmpDir)
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return dst, tmpDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", "flux-artifact-*")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
visited := make(map[string]bool)
|
||||||
|
if err := copyDir(absPath, tmpDir, visited); err != nil {
|
||||||
|
os.RemoveAll(tmpDir)
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmpDir, tmpDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyDir recursively copies the contents of srcDir to dstDir, resolving any
|
||||||
|
// symlinks encountered along the way. The visited map tracks resolved real
|
||||||
|
// directory paths to detect and break symlink cycles.
|
||||||
|
func copyDir(srcDir, dstDir string, visited map[string]bool) error {
|
||||||
|
real, err := filepath.EvalSymlinks(srcDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolving symlink %s: %w", srcDir, err)
|
||||||
|
}
|
||||||
|
abs, err := filepath.Abs(real)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting absolute path for %s: %w", real, err)
|
||||||
|
}
|
||||||
|
if visited[abs] {
|
||||||
|
return nil // break the cycle
|
||||||
|
}
|
||||||
|
visited[abs] = true
|
||||||
|
defer delete(visited, abs)
|
||||||
|
entries, err := os.ReadDir(srcDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
srcPath := filepath.Join(srcDir, entry.Name())
|
||||||
|
dstPath := filepath.Join(dstDir, entry.Name())
|
||||||
|
|
||||||
|
// Resolve symlinks to get the real path and info.
|
||||||
|
realPath, err := filepath.EvalSymlinks(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolving symlink %s: %w", srcPath, err)
|
||||||
|
}
|
||||||
|
realInfo, err := os.Stat(realPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stat resolved path %s: %w", realPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if realInfo.IsDir() {
|
||||||
|
if err := os.MkdirAll(dstPath, realInfo.Mode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Recursively copy the resolved directory contents.
|
||||||
|
if err := copyDir(realPath, dstPath, visited); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !realInfo.Mode().IsRegular() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := copyFile(realPath, dstPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
srcInfo, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(out, in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return out.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func saveReaderToFile(reader io.Reader) (string, error) {
|
func saveReaderToFile(reader io.Reader) (string, error) {
|
||||||
b, err := io.ReadAll(bufio.NewReader(reader))
|
b, err := io.ReadAll(bufio.NewReader(reader))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -68,3 +69,149 @@ data:
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_resolveSymlinks(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
// Create source directory with a real file
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
realFile := filepath.Join(srcDir, "real.yaml")
|
||||||
|
g.Expect(os.WriteFile(realFile, []byte("apiVersion: v1\nkind: Namespace\nmetadata:\n name: test\n"), 0o644)).To(Succeed())
|
||||||
|
|
||||||
|
// Create a directory with symlinks pointing to files outside it
|
||||||
|
symlinkDir := t.TempDir()
|
||||||
|
symlinkFile := filepath.Join(symlinkDir, "linked.yaml")
|
||||||
|
g.Expect(os.Symlink(realFile, symlinkFile)).To(Succeed())
|
||||||
|
|
||||||
|
// Also add a regular file in the symlink dir
|
||||||
|
regularFile := filepath.Join(symlinkDir, "regular.yaml")
|
||||||
|
g.Expect(os.WriteFile(regularFile, []byte("apiVersion: v1\nkind: ConfigMap\n"), 0o644)).To(Succeed())
|
||||||
|
|
||||||
|
// Create a symlinked subdirectory
|
||||||
|
subDir := filepath.Join(srcDir, "subdir")
|
||||||
|
g.Expect(os.MkdirAll(subDir, 0o755)).To(Succeed())
|
||||||
|
g.Expect(os.WriteFile(filepath.Join(subDir, "nested.yaml"), []byte("nested"), 0o644)).To(Succeed())
|
||||||
|
g.Expect(os.Symlink(subDir, filepath.Join(symlinkDir, "linkeddir"))).To(Succeed())
|
||||||
|
|
||||||
|
// Resolve symlinks
|
||||||
|
resolved, cleanupDir, err := resolveSymlinks(symlinkDir)
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
||||||
|
|
||||||
|
// Verify the regular file was copied
|
||||||
|
content, err := os.ReadFile(filepath.Join(resolved, "regular.yaml"))
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
g.Expect(string(content)).To(Equal("apiVersion: v1\nkind: ConfigMap\n"))
|
||||||
|
|
||||||
|
// Verify the symlinked file was resolved and copied
|
||||||
|
content, err = os.ReadFile(filepath.Join(resolved, "linked.yaml"))
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
g.Expect(string(content)).To(ContainSubstring("kind: Namespace"))
|
||||||
|
|
||||||
|
// Verify that the resolved file is a regular file, not a symlink
|
||||||
|
info, err := os.Lstat(filepath.Join(resolved, "linked.yaml"))
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
g.Expect(info.Mode().IsRegular()).To(BeTrue())
|
||||||
|
|
||||||
|
// Verify that the symlinked directory was resolved and its contents were copied
|
||||||
|
content, err = os.ReadFile(filepath.Join(resolved, "linkeddir", "nested.yaml"))
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
g.Expect(string(content)).To(Equal("nested"))
|
||||||
|
|
||||||
|
// Verify that the file inside the symlinked directory is a regular file
|
||||||
|
info, err = os.Lstat(filepath.Join(resolved, "linkeddir", "nested.yaml"))
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
g.Expect(info.Mode().IsRegular()).To(BeTrue())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_resolveSymlinks_singleFile(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
// Create a real file
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
realFile := filepath.Join(srcDir, "manifest.yaml")
|
||||||
|
g.Expect(os.WriteFile(realFile, []byte("kind: ConfigMap"), 0o644)).To(Succeed())
|
||||||
|
|
||||||
|
// Create a symlink to the real file
|
||||||
|
linkDir := t.TempDir()
|
||||||
|
linkFile := filepath.Join(linkDir, "link.yaml")
|
||||||
|
g.Expect(os.Symlink(realFile, linkFile)).To(Succeed())
|
||||||
|
|
||||||
|
// Resolve the single symlinked file
|
||||||
|
resolved, cleanupDir, err := resolveSymlinks(linkFile)
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
||||||
|
|
||||||
|
// The returned path should be a file, not a directory
|
||||||
|
info, err := os.Stat(resolved)
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
g.Expect(info.IsDir()).To(BeFalse())
|
||||||
|
|
||||||
|
// Verify contents
|
||||||
|
content, err := os.ReadFile(resolved)
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
g.Expect(string(content)).To(Equal("kind: ConfigMap"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_resolveSymlinks_cycle(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
// Create a directory with a symlink cycle: dir/link -> dir
|
||||||
|
dir := t.TempDir()
|
||||||
|
g.Expect(os.WriteFile(filepath.Join(dir, "file.yaml"), []byte("data"), 0o644)).To(Succeed())
|
||||||
|
g.Expect(os.Symlink(dir, filepath.Join(dir, "cycle"))).To(Succeed())
|
||||||
|
|
||||||
|
// resolveSymlinks should not infinite-loop
|
||||||
|
resolved, cleanupDir, err := resolveSymlinks(dir)
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
||||||
|
|
||||||
|
// The file should be copied
|
||||||
|
content, err := os.ReadFile(filepath.Join(resolved, "file.yaml"))
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
g.Expect(string(content)).To(Equal("data"))
|
||||||
|
|
||||||
|
// The cycle directory should exist but not cause infinite nesting
|
||||||
|
_, err = os.Stat(filepath.Join(resolved, "cycle"))
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
// There should NOT be deeply nested cycle/cycle/cycle/... paths
|
||||||
|
_, err = os.Stat(filepath.Join(resolved, "cycle", "cycle", "cycle"))
|
||||||
|
g.Expect(os.IsNotExist(err)).To(BeTrue())
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_resolveSymlinks_multipleLinksSameTarget(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
// Create source directory with a real file inside a dir
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
targetDir := filepath.Join(srcDir, "target")
|
||||||
|
g.Expect(os.MkdirAll(targetDir, 0o755)).To(Succeed())
|
||||||
|
g.Expect(os.WriteFile(filepath.Join(targetDir, "file.yaml"), []byte("data"), 0o644)).To(Succeed())
|
||||||
|
|
||||||
|
// Create a directory with multiple symlinks pointing to targetDir
|
||||||
|
symlinkDir := t.TempDir()
|
||||||
|
|
||||||
|
// Link 1
|
||||||
|
link1 := filepath.Join(symlinkDir, "link1")
|
||||||
|
g.Expect(os.Symlink(targetDir, link1)).To(Succeed())
|
||||||
|
|
||||||
|
// Link 2
|
||||||
|
link2 := filepath.Join(symlinkDir, "link2")
|
||||||
|
g.Expect(os.Symlink(targetDir, link2)).To(Succeed())
|
||||||
|
|
||||||
|
// Resolve symlinks
|
||||||
|
resolved, cleanupDir, err := resolveSymlinks(symlinkDir)
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
||||||
|
|
||||||
|
// Verify link1 has the file
|
||||||
|
content, err := os.ReadFile(filepath.Join(resolved, "link1", "file.yaml"))
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
g.Expect(string(content)).To(Equal("data"))
|
||||||
|
|
||||||
|
// Verify link2 ALSO has the file
|
||||||
|
content2, err := os.ReadFile(filepath.Join(resolved, "link2", "file.yaml"))
|
||||||
|
g.Expect(err).To(BeNil())
|
||||||
|
g.Expect(string(content2)).To(Equal("data"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ type buildKsFlags struct {
|
|||||||
strictSubst bool
|
strictSubst bool
|
||||||
recursive bool
|
recursive bool
|
||||||
localSources map[string]string
|
localSources map[string]string
|
||||||
|
inMemoryBuild bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var buildKsArgs buildKsFlags
|
var buildKsArgs buildKsFlags
|
||||||
@@ -85,6 +86,8 @@ func init() {
|
|||||||
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
||||||
buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations")
|
buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations")
|
||||||
buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
||||||
|
buildKsCmd.Flags().BoolVar(&buildKsArgs.inMemoryBuild, "in-memory-build", true,
|
||||||
|
"Use in-memory filesystem during build.")
|
||||||
buildCmd.AddCommand(buildKsCmd)
|
buildCmd.AddCommand(buildKsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +133,7 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
|
|||||||
build.WithStrictSubstitute(buildKsArgs.strictSubst),
|
build.WithStrictSubstitute(buildKsArgs.strictSubst),
|
||||||
build.WithRecursive(buildKsArgs.recursive),
|
build.WithRecursive(buildKsArgs.recursive),
|
||||||
build.WithLocalSources(buildKsArgs.localSources),
|
build.WithLocalSources(buildKsArgs.localSources),
|
||||||
|
build.WithInMemoryBuild(buildKsArgs.inMemoryBuild),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
builder, err = build.NewBuilder(name, buildKsArgs.path,
|
builder, err = build.NewBuilder(name, buildKsArgs.path,
|
||||||
@@ -140,6 +144,7 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
|
|||||||
build.WithStrictSubstitute(buildKsArgs.strictSubst),
|
build.WithStrictSubstitute(buildKsArgs.strictSubst),
|
||||||
build.WithRecursive(buildKsArgs.recursive),
|
build.WithRecursive(buildKsArgs.recursive),
|
||||||
build.WithLocalSources(buildKsArgs.localSources),
|
build.WithLocalSources(buildKsArgs.localSources),
|
||||||
|
build.WithInMemoryBuild(buildKsArgs.inMemoryBuild),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ func TestBuildKustomization(t *testing.T) {
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build podinfo (on-disk)",
|
||||||
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "build podinfo without service",
|
name: "build podinfo without service",
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service",
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service",
|
||||||
@@ -70,12 +76,24 @@ func TestBuildKustomization(t *testing.T) {
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build ignore (on-disk)",
|
||||||
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\" --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "build with recursive",
|
name: "build with recursive",
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build with recursive (on-disk)",
|
||||||
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := map[string]string{
|
tmpl := map[string]string{
|
||||||
@@ -145,6 +163,12 @@ spec:
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build podinfo (on-disk)",
|
||||||
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "build podinfo without service",
|
name: "build podinfo without service",
|
||||||
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
|
||||||
@@ -175,6 +199,18 @@ spec:
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build with recursive (on-disk)",
|
||||||
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "build with recursive in dry-run mode (on-disk)",
|
||||||
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false --dry-run",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := map[string]string{
|
tmpl := map[string]string{
|
||||||
@@ -241,6 +277,12 @@ func TestBuildKustomizationPathNormalization(t *testing.T) {
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "build with absolute path (on-disk)",
|
||||||
|
args: "build kustomization podinfo --path " + absTestDataPath + " --in-memory-build=false",
|
||||||
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "build with complex relative path (parent dir)",
|
name: "build with complex relative path (parent dir)",
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo",
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo",
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ type checkFlags struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var kubernetesConstraints = []string{
|
var kubernetesConstraints = []string{
|
||||||
">=1.32.0-0",
|
">=1.33.0-0",
|
||||||
}
|
}
|
||||||
|
|
||||||
var checkArgs checkFlags
|
var checkArgs checkFlags
|
||||||
|
|||||||
@@ -136,6 +136,9 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
if !strings.HasPrefix(kustomizationArgs.path.String(), "./") {
|
if !strings.HasPrefix(kustomizationArgs.path.String(), "./") {
|
||||||
return fmt.Errorf("path must begin with ./")
|
return fmt.Errorf("path must begin with ./")
|
||||||
}
|
}
|
||||||
|
if kustomizationArgs.source.Name == "" {
|
||||||
|
return fmt.Errorf("source is required")
|
||||||
|
}
|
||||||
|
|
||||||
if !createArgs.export {
|
if !createArgs.export {
|
||||||
logger.Generatef("generating Kustomization")
|
logger.Generatef("generating Kustomization")
|
||||||
|
|||||||
48
cmd/flux/create_kustomization_test.go
Normal file
48
cmd/flux/create_kustomization_test.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
//go:build unit
|
||||||
|
// +build unit
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCreateKustomization(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
assert assertFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// A user creating a kustomization without --source gets a confusing
|
||||||
|
// API-level error about spec.sourceRef.kind instead of a clear message.
|
||||||
|
name: "missing source",
|
||||||
|
args: "create kustomization my-app --path=./deploy --export",
|
||||||
|
assert: assertError("source is required"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: tt.args,
|
||||||
|
assert: tt.assert,
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ var createReceiverCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type receiverFlags struct {
|
type receiverFlags struct {
|
||||||
receiverType string
|
receiverType flags.ReceiverType
|
||||||
secretRef string
|
secretRef string
|
||||||
events []string
|
events []string
|
||||||
resources []string
|
resources []string
|
||||||
@@ -58,7 +59,7 @@ type receiverFlags struct {
|
|||||||
var receiverArgs receiverFlags
|
var receiverArgs receiverFlags
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
createReceiverCmd.Flags().StringVar(&receiverArgs.receiverType, "type", "", "")
|
createReceiverCmd.Flags().Var(&receiverArgs.receiverType, "type", receiverArgs.receiverType.Description())
|
||||||
createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "")
|
createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "")
|
||||||
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values")
|
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values")
|
||||||
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values")
|
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values")
|
||||||
@@ -109,7 +110,7 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
Labels: sourceLabels,
|
Labels: sourceLabels,
|
||||||
},
|
},
|
||||||
Spec: notificationv1.ReceiverSpec{
|
Spec: notificationv1.ReceiverSpec{
|
||||||
Type: receiverArgs.receiverType,
|
Type: receiverArgs.receiverType.String(),
|
||||||
Events: receiverArgs.events,
|
Events: receiverArgs.events,
|
||||||
Resources: resources,
|
Resources: resources,
|
||||||
SecretRef: meta.LocalObjectReference{
|
SecretRef: meta.LocalObjectReference{
|
||||||
|
|||||||
@@ -56,6 +56,22 @@ func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
existing.StringData = secret.StringData
|
existing.StringData = secret.StringData
|
||||||
|
if secret.Annotations != nil {
|
||||||
|
if existing.Annotations == nil {
|
||||||
|
existing.Annotations = make(map[string]string)
|
||||||
|
}
|
||||||
|
for k, v := range secret.Annotations {
|
||||||
|
existing.Annotations[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if secret.Labels != nil {
|
||||||
|
if existing.Labels == nil {
|
||||||
|
existing.Labels = make(map[string]string)
|
||||||
|
}
|
||||||
|
for k, v := range secret.Labels {
|
||||||
|
existing.Labels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := kubeClient.Update(ctx, &existing); err != nil {
|
if err := kubeClient.Update(ctx, &existing); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
134
cmd/flux/create_secret_receiver.go
Normal file
134
cmd/flux/create_secret_receiver.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/flags"
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
|
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
|
||||||
|
)
|
||||||
|
|
||||||
|
var createSecretReceiverCmd = &cobra.Command{
|
||||||
|
Use: "receiver [name]",
|
||||||
|
Short: "Create or update a Kubernetes secret for a Receiver webhook",
|
||||||
|
Long: `The create secret receiver command generates a Kubernetes secret with
|
||||||
|
the token used for webhook payload validation and an annotation with the
|
||||||
|
computed webhook URL.`,
|
||||||
|
Example: ` # Create a receiver secret for a GitHub webhook
|
||||||
|
flux create secret receiver github-receiver \
|
||||||
|
--namespace=my-namespace \
|
||||||
|
--type=github \
|
||||||
|
--hostname=flux.example.com \
|
||||||
|
--export
|
||||||
|
|
||||||
|
# Create a receiver secret for GCR with email claim
|
||||||
|
flux create secret receiver gcr-receiver \
|
||||||
|
--namespace=my-namespace \
|
||||||
|
--type=gcr \
|
||||||
|
--hostname=flux.example.com \
|
||||||
|
--email-claim=sa@project.iam.gserviceaccount.com \
|
||||||
|
--export`,
|
||||||
|
RunE: createSecretReceiverCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
type secretReceiverFlags struct {
|
||||||
|
receiverType flags.ReceiverType
|
||||||
|
token string
|
||||||
|
hostname string
|
||||||
|
emailClaim string
|
||||||
|
audienceClaim string
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretReceiverArgs secretReceiverFlags
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
createSecretReceiverCmd.Flags().Var(&secretReceiverArgs.receiverType, "type", secretReceiverArgs.receiverType.Description())
|
||||||
|
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.token, "token", "", "webhook token used for payload validation and URL computation, auto-generated if not specified")
|
||||||
|
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.hostname, "hostname", "", "hostname for the webhook URL e.g. flux.example.com")
|
||||||
|
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.emailClaim, "email-claim", "", "IAM service account email, required for gcr type")
|
||||||
|
createSecretReceiverCmd.Flags().StringVar(&secretReceiverArgs.audienceClaim, "audience-claim", "", "custom OIDC token audience for gcr type, defaults to the webhook URL")
|
||||||
|
|
||||||
|
createSecretCmd.AddCommand(createSecretReceiverCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSecretReceiverCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
if secretReceiverArgs.receiverType == "" {
|
||||||
|
return fmt.Errorf("--type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretReceiverArgs.hostname == "" {
|
||||||
|
return fmt.Errorf("--hostname is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretReceiverArgs.receiverType.String() == notificationv1.GCRReceiver && secretReceiverArgs.emailClaim == "" {
|
||||||
|
return fmt.Errorf("--email-claim is required for gcr receiver type")
|
||||||
|
}
|
||||||
|
|
||||||
|
labels, err := parseLabels()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := sourcesecret.Options{
|
||||||
|
Name: name,
|
||||||
|
Namespace: *kubeconfigArgs.Namespace,
|
||||||
|
Labels: labels,
|
||||||
|
ReceiverType: secretReceiverArgs.receiverType.String(),
|
||||||
|
Token: secretReceiverArgs.token,
|
||||||
|
Hostname: secretReceiverArgs.hostname,
|
||||||
|
EmailClaim: secretReceiverArgs.emailClaim,
|
||||||
|
AudienceClaim: secretReceiverArgs.audienceClaim,
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := sourcesecret.GenerateReceiver(opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if createArgs.export {
|
||||||
|
rootCmd.Println(secret.Content)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
||||||
|
defer cancel()
|
||||||
|
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var s corev1.Secret
|
||||||
|
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := upsertSecret(ctx, kubeClient, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Actionf("receiver secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
74
cmd/flux/create_secret_receiver_test.go
Normal file
74
cmd/flux/create_secret_receiver_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateReceiverSecret(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
assert assertFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing type",
|
||||||
|
args: "create secret receiver test-secret --token=t --hostname=h",
|
||||||
|
assert: assertError("--type is required"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid type",
|
||||||
|
args: "create secret receiver test-secret --type=invalid --token=t --hostname=h",
|
||||||
|
assert: assertError("invalid argument \"invalid\" for \"--type\" flag: receiver type 'invalid' is not supported, must be one of: generic, generic-hmac, github, gitlab, bitbucket, harbor, dockerhub, quay, gcr, nexus, acr, cdevents"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing hostname",
|
||||||
|
args: "create secret receiver test-secret --type=github --token=t",
|
||||||
|
assert: assertError("--hostname is required"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gcr missing email-claim",
|
||||||
|
args: "create secret receiver test-secret --type=gcr --token=t --hostname=h",
|
||||||
|
assert: assertError("--email-claim is required for gcr receiver type"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "github receiver secret",
|
||||||
|
args: "create secret receiver receiver-secret --type=github --token=test-token --hostname=flux.example.com --namespace=my-namespace --export",
|
||||||
|
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gcr receiver secret",
|
||||||
|
args: "create secret receiver gcr-secret --type=gcr --token=test-token --hostname=flux.example.com --email-claim=sa@project.iam.gserviceaccount.com --namespace=my-namespace --export",
|
||||||
|
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver-gcr.yaml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gcr receiver secret with custom audience",
|
||||||
|
args: "create secret receiver gcr-secret --type=gcr --token=test-token --hostname=flux.example.com --email-claim=sa@project.iam.gserviceaccount.com --audience-claim=https://custom.audience.example.com --namespace=my-namespace --export",
|
||||||
|
assert: assertGoldenFile("testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: tt.args,
|
||||||
|
assert: tt.assert,
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,6 +62,8 @@ type diffKsFlags struct {
|
|||||||
strictSubst bool
|
strictSubst bool
|
||||||
recursive bool
|
recursive bool
|
||||||
localSources map[string]string
|
localSources map[string]string
|
||||||
|
inMemoryBuild bool
|
||||||
|
ignoreNotFound bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var diffKsArgs diffKsFlags
|
var diffKsArgs diffKsFlags
|
||||||
@@ -75,6 +77,10 @@ func init() {
|
|||||||
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
||||||
diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations")
|
diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations")
|
||||||
diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
||||||
|
diffKsCmd.Flags().BoolVar(&diffKsArgs.inMemoryBuild, "in-memory-build", true,
|
||||||
|
"Use in-memory filesystem during build.")
|
||||||
|
diffKsCmd.Flags().BoolVar(&diffKsArgs.ignoreNotFound, "ignore-not-found", false,
|
||||||
|
"Ignore Kustomization not found errors on the cluster when diffing.")
|
||||||
diffCmd.AddCommand(diffKsCmd)
|
diffCmd.AddCommand(diffKsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +119,8 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
build.WithRecursive(diffKsArgs.recursive),
|
build.WithRecursive(diffKsArgs.recursive),
|
||||||
build.WithLocalSources(diffKsArgs.localSources),
|
build.WithLocalSources(diffKsArgs.localSources),
|
||||||
build.WithSingleKustomization(),
|
build.WithSingleKustomization(),
|
||||||
|
build.WithInMemoryBuild(diffKsArgs.inMemoryBuild),
|
||||||
|
build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
builder, err = build.NewBuilder(name, diffKsArgs.path,
|
builder, err = build.NewBuilder(name, diffKsArgs.path,
|
||||||
@@ -124,6 +132,8 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
build.WithRecursive(diffKsArgs.recursive),
|
build.WithRecursive(diffKsArgs.recursive),
|
||||||
build.WithLocalSources(diffKsArgs.localSources),
|
build.WithLocalSources(diffKsArgs.localSources),
|
||||||
build.WithSingleKustomization(),
|
build.WithSingleKustomization(),
|
||||||
|
build.WithInMemoryBuild(diffKsArgs.inMemoryBuild),
|
||||||
|
build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func TestDiffKustomization(t *testing.T) {
|
|||||||
name: "diff nothing deployed",
|
name: "diff nothing deployed",
|
||||||
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false",
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false",
|
||||||
objectFile: "",
|
objectFile: "",
|
||||||
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "diff with a deployment object",
|
name: "diff with a deployment object",
|
||||||
@@ -96,7 +96,7 @@ func TestDiffKustomization(t *testing.T) {
|
|||||||
name: "diff where kustomization file has multiple objects with the same name",
|
name: "diff where kustomization file has multiple objects with the same name",
|
||||||
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml",
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml",
|
||||||
objectFile: "",
|
objectFile: "",
|
||||||
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "diff with recursive",
|
name: "diff with recursive",
|
||||||
@@ -138,6 +138,118 @@ func TestDiffKustomization(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationNotDeployed tests `flux diff ks` when the Kustomization
|
||||||
|
// CR does not exist in the cluster but is provided via --kustomization-file.
|
||||||
|
// Reproduces https://github.com/fluxcd/flux2/issues/5439
|
||||||
|
func TestDiffKustomizationNotDeployed(t *testing.T) {
|
||||||
|
// Use a dedicated namespace with NO setup() -- the Kustomization CR
|
||||||
|
// intentionally does not exist in the cluster.
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args string
|
||||||
|
assert assertFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fails without --ignore-not-found",
|
||||||
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml",
|
||||||
|
assert: assertError("failed to get kustomization object: kustomizations.kustomize.toolkit.fluxcd.io \"podinfo\" not found"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "succeeds with --ignore-not-found and --kustomization-file",
|
||||||
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-local-only.yaml " +
|
||||||
|
"--ignore-not-found",
|
||||||
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: tt.args + " -n " + tmpl["fluxns"],
|
||||||
|
assert: tt.assert,
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationTakeOwnership tests `flux diff ks` when taking ownership
|
||||||
|
// of existing resources on the cluster. A "pre-existing" configmap is applied
|
||||||
|
// to the cluster, and the kustomization contains a matching configmap; the
|
||||||
|
// diff should show the labels added by flux
|
||||||
|
func TestDiffKustomizationTakeOwnership(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
b, _ := build.NewBuilder("configmaps", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions))
|
||||||
|
resourceManager, err := b.Manager()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-create the "existing" configmap in the cluster without Flux labels
|
||||||
|
if _, err := resourceManager.ApplyAll(context.Background(), createObjectFromFile("./testdata/diff-kustomization/existing-configmap.yaml", tmpl, t), ssa.DefaultApplyOptions()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "diff kustomization configmaps --path ./testdata/build-kustomization/configmaps --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-configmaps.yaml " +
|
||||||
|
"--ignore-not-found" +
|
||||||
|
" -n " + tmpl["fluxns"],
|
||||||
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-taking-ownership.golden"),
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationNewNamespaceAndConfigmap runs `flux diff ks` when the
|
||||||
|
// kustomization creates a new namespace and resources inside it. The server-side
|
||||||
|
// dry-run cannot resolve resources in a namespace that doesn't exist yet,
|
||||||
|
// consistent with `kubectl diff --server-side` behavior.
|
||||||
|
func TestDiffKustomizationNewNamespaceAndConfigmap(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "diff kustomization new-namespace-and-configmap --path ./testdata/build-kustomization/new-namespace-and-configmap --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml " +
|
||||||
|
"--ignore-not-found" +
|
||||||
|
" -n " + tmpl["fluxns"],
|
||||||
|
assert: assertError("ConfigMap/new-ns/app-config not found: namespaces \"new-ns\" not found"),
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDiffKustomizationNewNamespaceOnly runs `flux diff ks` when the
|
||||||
|
// kustomization creates only a new namespace. The diff should show the
|
||||||
|
// namespace as created.
|
||||||
|
func TestDiffKustomizationNewNamespaceOnly(t *testing.T) {
|
||||||
|
tmpl := map[string]string{
|
||||||
|
"fluxns": allocateNamespace("flux-system"),
|
||||||
|
}
|
||||||
|
setupTestNamespace(tmpl["fluxns"], t)
|
||||||
|
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "diff kustomization new-namespace-only --path ./testdata/build-kustomization/new-namespace-only --progress-bar=false " +
|
||||||
|
"--kustomization-file ./testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml " +
|
||||||
|
"--ignore-not-found" +
|
||||||
|
" -n " + tmpl["fluxns"],
|
||||||
|
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-namespace-only.golden"),
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
}
|
||||||
|
|
||||||
func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured {
|
func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured {
|
||||||
buf, err := os.ReadFile(objectFile)
|
buf, err := os.ReadFile(objectFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -196,11 +196,14 @@ func getRows(ctx context.Context, kubeclient client.Client, clientListOpts []cli
|
|||||||
|
|
||||||
func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error {
|
func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error {
|
||||||
listOpts := &metav1.ListOptions{}
|
listOpts := &metav1.ListOptions{}
|
||||||
clientListOpts = append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
|
|
||||||
err := runtimeresource.FollowContinue(listOpts,
|
err := runtimeresource.FollowContinue(listOpts,
|
||||||
func(options metav1.ListOptions) (runtime.Object, error) {
|
func(options metav1.ListOptions) (runtime.Object, error) {
|
||||||
newEvents := &corev1.EventList{}
|
newEvents := &corev1.EventList{}
|
||||||
if err := kubeclient.List(ctx, newEvents, clientListOpts...); err != nil {
|
opts := append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
|
||||||
|
if options.Continue != "" {
|
||||||
|
opts = append(opts, client.Continue(options.Continue))
|
||||||
|
}
|
||||||
|
if err := kubeclient.List(ctx, newEvents, opts...); err != nil {
|
||||||
return nil, fmt.Errorf("error getting events: %w", err)
|
return nil, fmt.Errorf("error getting events: %w", err)
|
||||||
}
|
}
|
||||||
el.Items = append(el.Items, newEvents.Items...)
|
el.Items = append(el.Items, newEvents.Items...)
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
@@ -419,6 +421,108 @@ func createEvent(obj client.Object, eventType, msg, reason string) corev1.Event
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// paginatedClient wraps a client.Client and simulates real Kubernetes API
|
||||||
|
// pagination by splitting List results into pages of pageSize items,
|
||||||
|
// using the ListMeta.Continue token.
|
||||||
|
type paginatedClient struct {
|
||||||
|
client.Client
|
||||||
|
pageSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *paginatedClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
|
||||||
|
listOpts := &client.ListOptions{}
|
||||||
|
listOpts.ApplyOptions(opts)
|
||||||
|
|
||||||
|
// Fetch all results from the underlying client (without Limit/Continue).
|
||||||
|
stripped := make([]client.ListOption, 0, len(opts))
|
||||||
|
for _, o := range opts {
|
||||||
|
if _, ok := o.(client.Limit); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := o.(client.Continue); ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stripped = append(stripped, o)
|
||||||
|
}
|
||||||
|
if err := c.Client.List(ctx, list, stripped...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
items, err := meta.ExtractList(list)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the page window based on the Continue token.
|
||||||
|
start := 0
|
||||||
|
if listOpts.Continue != "" {
|
||||||
|
n, err := strconv.Atoi(listOpts.Continue)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid continue token: %w", err)
|
||||||
|
}
|
||||||
|
start = n
|
||||||
|
}
|
||||||
|
if start > len(items) {
|
||||||
|
start = len(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
end := start + c.pageSize
|
||||||
|
if end > len(items) {
|
||||||
|
end = len(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
page := items[start:end]
|
||||||
|
if err := meta.SetList(list, page); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the Continue token when there are more pages.
|
||||||
|
listAccessor, err := meta.ListAccessor(list)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if end < len(items) {
|
||||||
|
listAccessor.SetContinue(strconv.Itoa(end))
|
||||||
|
} else {
|
||||||
|
listAccessor.SetContinue("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_addEventsToList_pagination(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
objs, err := ssautil.ReadObjects(strings.NewReader(objects))
|
||||||
|
g.Expect(err).To(Not(HaveOccurred()))
|
||||||
|
|
||||||
|
builder := fake.NewClientBuilder().WithScheme(utils.NewScheme())
|
||||||
|
for _, obj := range objs {
|
||||||
|
builder = builder.WithObjects(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
eventList := &corev1.EventList{}
|
||||||
|
for _, obj := range objs {
|
||||||
|
infoEvent := createEvent(obj, eventv1.EventSeverityInfo, "Info Message", "Info Reason")
|
||||||
|
warningEvent := createEvent(obj, eventv1.EventSeverityError, "Error Message", "Error Reason")
|
||||||
|
eventList.Items = append(eventList.Items, infoEvent, warningEvent)
|
||||||
|
}
|
||||||
|
builder = builder.WithLists(eventList)
|
||||||
|
c := builder.Build()
|
||||||
|
|
||||||
|
totalEvents := len(eventList.Items)
|
||||||
|
g.Expect(totalEvents).To(BeNumerically(">", 2), "need more than 2 events to test pagination")
|
||||||
|
|
||||||
|
// Wrap the client to paginate at 2 items per page, forcing multiple
|
||||||
|
// round-trips through FollowContinue.
|
||||||
|
pc := &paginatedClient{Client: c, pageSize: 2}
|
||||||
|
|
||||||
|
el := &corev1.EventList{}
|
||||||
|
err = addEventsToList(context.Background(), pc, el, nil)
|
||||||
|
g.Expect(err).To(Not(HaveOccurred()))
|
||||||
|
g.Expect(el.Items).To(HaveLen(totalEvents),
|
||||||
|
"addEventsToList should collect all events across paginated responses")
|
||||||
|
}
|
||||||
|
|
||||||
func kindNameIndexer(obj client.Object) []string {
|
func kindNameIndexer(obj client.Object) []string {
|
||||||
e, ok := obj.(*corev1.Event)
|
e, ok := obj.(*corev1.Event)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -28,13 +28,22 @@ import (
|
|||||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type getHelmReleaseFlags struct {
|
||||||
|
showSource bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var getHrArgs getHelmReleaseFlags
|
||||||
|
|
||||||
var getHelmReleaseCmd = &cobra.Command{
|
var getHelmReleaseCmd = &cobra.Command{
|
||||||
Use: "helmreleases",
|
Use: "helmreleases",
|
||||||
Aliases: []string{"hr", "helmrelease"},
|
Aliases: []string{"hr", "helmrelease"},
|
||||||
Short: "Get HelmRelease statuses",
|
Short: "Get HelmRelease statuses",
|
||||||
Long: "The get helmreleases command prints the statuses of the resources.",
|
Long: "The get helmreleases command prints the statuses of the resources.",
|
||||||
Example: ` # List all Helm releases and their status
|
Example: ` # List all Helm releases and their status
|
||||||
flux get helmreleases`,
|
flux get helmreleases
|
||||||
|
|
||||||
|
# List all Helm releases with source information
|
||||||
|
flux get helmreleases --show-source`,
|
||||||
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),
|
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
get := getCommand{
|
get := getCommand{
|
||||||
@@ -69,6 +78,7 @@ var getHelmReleaseCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
getHelmReleaseCmd.Flags().BoolVar(&getHrArgs.showSource, "show-source", false, "show the source reference for each helmrelease")
|
||||||
getCmd.AddCommand(getHelmReleaseCmd)
|
getCmd.AddCommand(getHelmReleaseCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,16 +89,45 @@ func getHelmReleaseRevision(helmRelease helmv2.HelmRelease) string {
|
|||||||
return helmRelease.Status.LastAttemptedRevision
|
return helmRelease.Status.LastAttemptedRevision
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getHelmReleaseSource(item helmv2.HelmRelease) string {
|
||||||
|
if item.Spec.ChartRef != nil {
|
||||||
|
ns := item.Spec.ChartRef.Namespace
|
||||||
|
if ns == "" {
|
||||||
|
ns = item.GetNamespace()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s/%s",
|
||||||
|
item.Spec.ChartRef.Kind,
|
||||||
|
ns,
|
||||||
|
item.Spec.ChartRef.Name)
|
||||||
|
}
|
||||||
|
ns := item.Spec.Chart.Spec.SourceRef.Namespace
|
||||||
|
if ns == "" {
|
||||||
|
ns = item.GetNamespace()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s/%s",
|
||||||
|
item.Spec.Chart.Spec.SourceRef.Kind,
|
||||||
|
ns,
|
||||||
|
item.Spec.Chart.Spec.SourceRef.Name)
|
||||||
|
}
|
||||||
|
|
||||||
func (a helmReleaseListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
func (a helmReleaseListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
||||||
item := a.Items[i]
|
item := a.Items[i]
|
||||||
revision := getHelmReleaseRevision(item)
|
revision := getHelmReleaseRevision(item)
|
||||||
status, msg := statusAndMessage(item.Status.Conditions)
|
status, msg := statusAndMessage(item.Status.Conditions)
|
||||||
return append(nameColumns(&item, includeNamespace, includeKind),
|
row := nameColumns(&item, includeNamespace, includeKind)
|
||||||
|
if getHrArgs.showSource {
|
||||||
|
row = append(row, getHelmReleaseSource(item))
|
||||||
|
}
|
||||||
|
return append(row,
|
||||||
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
|
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a helmReleaseListAdapter) headers(includeNamespace bool) []string {
|
func (a helmReleaseListAdapter) headers(includeNamespace bool) []string {
|
||||||
headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"}
|
headers := []string{"Name"}
|
||||||
|
if getHrArgs.showSource {
|
||||||
|
headers = append(headers, "Source")
|
||||||
|
}
|
||||||
|
headers = append(headers, "Revision", "Suspended", "Ready", "Message")
|
||||||
if includeNamespace {
|
if includeNamespace {
|
||||||
headers = append([]string{"Namespace"}, headers...)
|
headers = append([]string{"Namespace"}, headers...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,13 +30,22 @@ import (
|
|||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type getKustomizationFlags struct {
|
||||||
|
showSource bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var getKsArgs getKustomizationFlags
|
||||||
|
|
||||||
var getKsCmd = &cobra.Command{
|
var getKsCmd = &cobra.Command{
|
||||||
Use: "kustomizations",
|
Use: "kustomizations",
|
||||||
Aliases: []string{"ks", "kustomization"},
|
Aliases: []string{"ks", "kustomization"},
|
||||||
Short: "Get Kustomization statuses",
|
Short: "Get Kustomization statuses",
|
||||||
Long: `The get kustomizations command prints the statuses of the resources.`,
|
Long: `The get kustomizations command prints the statuses of the resources.`,
|
||||||
Example: ` # List all kustomizations and their status
|
Example: ` # List all kustomizations and their status
|
||||||
flux get kustomizations`,
|
flux get kustomizations
|
||||||
|
|
||||||
|
# List all kustomizations with source information
|
||||||
|
flux get kustomizations --show-source`,
|
||||||
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
|
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
get := getCommand{
|
get := getCommand{
|
||||||
@@ -74,6 +83,7 @@ var getKsCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
getKsCmd.Flags().BoolVar(&getKsArgs.showSource, "show-source", false, "show the source reference for each kustomization")
|
||||||
getCmd.AddCommand(getKsCmd)
|
getCmd.AddCommand(getKsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,12 +93,27 @@ func (a kustomizationListAdapter) summariseItem(i int, includeNamespace bool, in
|
|||||||
status, msg := statusAndMessage(item.Status.Conditions)
|
status, msg := statusAndMessage(item.Status.Conditions)
|
||||||
revision = utils.TruncateHex(revision)
|
revision = utils.TruncateHex(revision)
|
||||||
msg = utils.TruncateHex(msg)
|
msg = utils.TruncateHex(msg)
|
||||||
return append(nameColumns(&item, includeNamespace, includeKind),
|
row := nameColumns(&item, includeNamespace, includeKind)
|
||||||
|
if getKsArgs.showSource {
|
||||||
|
sourceNs := item.Spec.SourceRef.Namespace
|
||||||
|
if sourceNs == "" {
|
||||||
|
sourceNs = item.GetNamespace()
|
||||||
|
}
|
||||||
|
row = append(row, fmt.Sprintf("%s/%s/%s",
|
||||||
|
item.Spec.SourceRef.Kind,
|
||||||
|
sourceNs,
|
||||||
|
item.Spec.SourceRef.Name))
|
||||||
|
}
|
||||||
|
return append(row,
|
||||||
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
|
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a kustomizationListAdapter) headers(includeNamespace bool) []string {
|
func (a kustomizationListAdapter) headers(includeNamespace bool) []string {
|
||||||
headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"}
|
headers := []string{"Name"}
|
||||||
|
if getKsArgs.showSource {
|
||||||
|
headers = append(headers, "Source")
|
||||||
|
}
|
||||||
|
headers = append(headers, "Revision", "Suspended", "Ready", "Message")
|
||||||
if includeNamespace {
|
if includeNamespace {
|
||||||
headers = append([]string{"Namespace"}, headers...)
|
headers = append([]string{"Namespace"}, headers...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,16 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`,
|
|||||||
# Uninstall Flux and delete CRDs
|
# Uninstall Flux and delete CRDs
|
||||||
flux uninstall`,
|
flux uninstall`,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// If opted in via --ns-follows-kube-context flag or
|
||||||
|
// FLUX_NS_FOLLOWS_KUBE_CONTEXT env var, and --namespace was not
|
||||||
|
// explicitly set, respect the namespace from the kubeconfig context.
|
||||||
|
if !cmd.Flags().Changed("namespace") &&
|
||||||
|
(rootArgs.nsFollowsKubeContext || os.Getenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT") != "") {
|
||||||
|
if ctxNs := getKubeconfigContextNamespace(kubeconfigArgs); ctxNs != "" {
|
||||||
|
*kubeconfigArgs.Namespace = ctxNs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ns, err := cmd.Flags().GetString("namespace")
|
ns, err := cmd.Flags().GetString("namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting namespace: %w", err)
|
return fmt.Errorf("error getting namespace: %w", err)
|
||||||
@@ -119,6 +129,7 @@ type rootFlags struct {
|
|||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
verbose bool
|
verbose bool
|
||||||
pollInterval time.Duration
|
pollInterval time.Duration
|
||||||
|
nsFollowsKubeContext bool
|
||||||
defaults install.Options
|
defaults install.Options
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +150,8 @@ var kubeclientOptions = new(runclient.Options)
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation")
|
rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation")
|
||||||
rootCmd.PersistentFlags().BoolVar(&rootArgs.verbose, "verbose", false, "print generated objects")
|
rootCmd.PersistentFlags().BoolVar(&rootArgs.verbose, "verbose", false, "print generated objects")
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&rootArgs.nsFollowsKubeContext, "ns-follows-kube-context", false,
|
||||||
|
"use the namespace from the kubeconfig context instead of the default flux-system namespace, can also be set via FLUX_NS_FOLLOWS_KUBE_CONTEXT env var")
|
||||||
|
|
||||||
configureDefaultNamespace()
|
configureDefaultNamespace()
|
||||||
kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag
|
kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag
|
||||||
@@ -186,6 +199,8 @@ func main() {
|
|||||||
// logger, we configure it's logger to do nothing.
|
// logger, we configure it's logger to do nothing.
|
||||||
ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{}))
|
ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{}))
|
||||||
|
|
||||||
|
registerPlugins()
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
|
||||||
if err, ok := err.(*RequestError); ok {
|
if err, ok := err.(*RequestError); ok {
|
||||||
@@ -203,6 +218,26 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getKubeconfigContextNamespace returns the namespace from the current
|
||||||
|
// kubeconfig context, or an empty string if it cannot be determined.
|
||||||
|
func getKubeconfigContextNamespace(cf *genericclioptions.ConfigFlags) string {
|
||||||
|
rawConfig, err := cf.ToRawKubeConfigLoader().RawConfig()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
currentContext := rawConfig.CurrentContext
|
||||||
|
if cf.Context != nil && *cf.Context != "" {
|
||||||
|
currentContext = *cf.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx, ok := rawConfig.Contexts[currentContext]; ok {
|
||||||
|
return ctx.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func configureDefaultNamespace() {
|
func configureDefaultNamespace() {
|
||||||
*kubeconfigArgs.Namespace = rootArgs.defaults.Namespace
|
*kubeconfigArgs.Namespace = rootArgs.defaults.Namespace
|
||||||
fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE")
|
fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE")
|
||||||
|
|||||||
221
cmd/flux/main_context_ns_test.go
Normal file
221
cmd/flux/main_context_ns_test.go
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetKubeconfigContextNamespace(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
kubeconfig string
|
||||||
|
context string
|
||||||
|
expectedResult string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "returns namespace from current context",
|
||||||
|
kubeconfig: `apiVersion: v1
|
||||||
|
kind: Config
|
||||||
|
current-context: my-context
|
||||||
|
contexts:
|
||||||
|
- name: my-context
|
||||||
|
context:
|
||||||
|
cluster: my-cluster
|
||||||
|
namespace: custom-ns
|
||||||
|
clusters:
|
||||||
|
- name: my-cluster
|
||||||
|
cluster:
|
||||||
|
server: https://localhost:6443
|
||||||
|
`,
|
||||||
|
expectedResult: "custom-ns",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns empty when context has no namespace",
|
||||||
|
kubeconfig: `apiVersion: v1
|
||||||
|
kind: Config
|
||||||
|
current-context: my-context
|
||||||
|
contexts:
|
||||||
|
- name: my-context
|
||||||
|
context:
|
||||||
|
cluster: my-cluster
|
||||||
|
clusters:
|
||||||
|
- name: my-cluster
|
||||||
|
cluster:
|
||||||
|
server: https://localhost:6443
|
||||||
|
`,
|
||||||
|
expectedResult: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns namespace from context specified via --context flag",
|
||||||
|
kubeconfig: `apiVersion: v1
|
||||||
|
kind: Config
|
||||||
|
current-context: default-context
|
||||||
|
contexts:
|
||||||
|
- name: default-context
|
||||||
|
context:
|
||||||
|
cluster: my-cluster
|
||||||
|
namespace: default-ns
|
||||||
|
- name: other-context
|
||||||
|
context:
|
||||||
|
cluster: my-cluster
|
||||||
|
namespace: other-ns
|
||||||
|
clusters:
|
||||||
|
- name: my-cluster
|
||||||
|
cluster:
|
||||||
|
server: https://localhost:6443
|
||||||
|
`,
|
||||||
|
context: "other-context",
|
||||||
|
expectedResult: "other-ns",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns empty when context does not exist",
|
||||||
|
kubeconfig: `apiVersion: v1
|
||||||
|
kind: Config
|
||||||
|
current-context: non-existent
|
||||||
|
contexts: []
|
||||||
|
clusters: []
|
||||||
|
`,
|
||||||
|
expectedResult: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
// Write temporary kubeconfig.
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
kcPath := filepath.Join(tmpDir, "kubeconfig")
|
||||||
|
g.Expect(os.WriteFile(kcPath, []byte(tt.kubeconfig), 0o600)).To(Succeed())
|
||||||
|
|
||||||
|
// Use a local ConfigFlags instance to avoid polluting the
|
||||||
|
// package-global kubeconfigArgs (which caches a clientConfig
|
||||||
|
// internally and would leak state across tests).
|
||||||
|
cf := genericclioptions.NewConfigFlags(false)
|
||||||
|
cf.KubeConfig = &kcPath
|
||||||
|
cf.Context = &tt.context
|
||||||
|
|
||||||
|
got := getKubeconfigContextNamespace(cf)
|
||||||
|
g.Expect(got).To(Equal(tt.expectedResult))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextNamespaceOptIn(t *testing.T) {
|
||||||
|
kubeconfig := `apiVersion: v1
|
||||||
|
kind: Config
|
||||||
|
current-context: my-context
|
||||||
|
contexts:
|
||||||
|
- name: my-context
|
||||||
|
context:
|
||||||
|
cluster: my-cluster
|
||||||
|
namespace: context-ns
|
||||||
|
clusters:
|
||||||
|
- name: my-cluster
|
||||||
|
cluster:
|
||||||
|
server: https://localhost:6443
|
||||||
|
`
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
nsFollowsFlag bool
|
||||||
|
nsFollowsEnv string
|
||||||
|
envNamespace string
|
||||||
|
flagNamespace string
|
||||||
|
expectedNamespace string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ignores context namespace when not opted in",
|
||||||
|
expectedNamespace: rootArgs.defaults.Namespace,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "uses context namespace when opted in via flag",
|
||||||
|
nsFollowsFlag: true,
|
||||||
|
expectedNamespace: "context-ns",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "uses context namespace when opted in via env var",
|
||||||
|
nsFollowsEnv: "1",
|
||||||
|
expectedNamespace: "context-ns",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "context namespace takes precedence over FLUX_SYSTEM_NAMESPACE when opted in",
|
||||||
|
nsFollowsFlag: true,
|
||||||
|
envNamespace: "env-ns",
|
||||||
|
expectedNamespace: "context-ns",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "FLUX_SYSTEM_NAMESPACE used when not opted in",
|
||||||
|
envNamespace: "env-ns",
|
||||||
|
expectedNamespace: "env-ns",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "--namespace flag takes precedence over context namespace",
|
||||||
|
nsFollowsFlag: true,
|
||||||
|
flagNamespace: "flag-ns",
|
||||||
|
expectedNamespace: "flag-ns",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
// Write temporary kubeconfig.
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
kcPath := filepath.Join(tmpDir, "kubeconfig")
|
||||||
|
g.Expect(os.WriteFile(kcPath, []byte(kubeconfig), 0o600)).To(Succeed())
|
||||||
|
|
||||||
|
// Use a local ConfigFlags instance to avoid polluting the
|
||||||
|
// package-global kubeconfigArgs.
|
||||||
|
cf := genericclioptions.NewConfigFlags(false)
|
||||||
|
cf.KubeConfig = &kcPath
|
||||||
|
emptyCtx := ""
|
||||||
|
cf.Context = &emptyCtx
|
||||||
|
|
||||||
|
// Mirror configureDefaultNamespace behavior on the local instance.
|
||||||
|
defaultNs := rootArgs.defaults.Namespace
|
||||||
|
cf.Namespace = &defaultNs
|
||||||
|
|
||||||
|
if tt.envNamespace != "" {
|
||||||
|
t.Setenv("FLUX_SYSTEM_NAMESPACE", tt.envNamespace)
|
||||||
|
envNs := tt.envNamespace
|
||||||
|
cf.Namespace = &envNs
|
||||||
|
}
|
||||||
|
if tt.nsFollowsEnv != "" {
|
||||||
|
t.Setenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT", tt.nsFollowsEnv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate PersistentPreRunE behavior.
|
||||||
|
if tt.flagNamespace != "" {
|
||||||
|
*cf.Namespace = tt.flagNamespace
|
||||||
|
} else if tt.nsFollowsFlag || os.Getenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT") != "" {
|
||||||
|
if ctxNs := getKubeconfigContextNamespace(cf); ctxNs != "" {
|
||||||
|
*cf.Namespace = ctxNs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Expect(*cf.Namespace).To(Equal(tt.expectedNamespace))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -374,6 +374,12 @@ func executeCommand(cmd string) (string, error) {
|
|||||||
// in subsequent executions which causes tests to fail that rely on the value
|
// in subsequent executions which causes tests to fail that rely on the value
|
||||||
// of "Changed".
|
// of "Changed".
|
||||||
resumeCmd.PersistentFlags().Lookup("wait").Changed = false
|
resumeCmd.PersistentFlags().Lookup("wait").Changed = false
|
||||||
|
// Reset the help flag value and Changed state so that a prior
|
||||||
|
// "--help" invocation does not leak into subsequent test runs.
|
||||||
|
if hf := rootCmd.Flags().Lookup("help"); hf != nil {
|
||||||
|
hf.Value.Set("false")
|
||||||
|
hf.Changed = false
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
args, err := shellwords.Parse(cmd)
|
args, err := shellwords.Parse(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -456,6 +462,7 @@ func resetCmdArgs() {
|
|||||||
secretGitArgs = NewSecretGitFlags()
|
secretGitArgs = NewSecretGitFlags()
|
||||||
secretGitHubAppArgs = secretGitHubAppFlags{}
|
secretGitHubAppArgs = secretGitHubAppFlags{}
|
||||||
secretProxyArgs = secretProxyFlags{}
|
secretProxyArgs = secretProxyFlags{}
|
||||||
|
secretReceiverArgs = secretReceiverFlags{}
|
||||||
secretHelmArgs = secretHelmFlags{}
|
secretHelmArgs = secretHelmFlags{}
|
||||||
secretTLSArgs = secretTLSFlags{}
|
secretTLSArgs = secretTLSFlags{}
|
||||||
sourceBucketArgs = sourceBucketFlags{}
|
sourceBucketArgs = sourceBucketFlags{}
|
||||||
|
|||||||
@@ -59,11 +59,26 @@ type APIVersions struct {
|
|||||||
|
|
||||||
// TODO: Update this mapping when new Flux minor versions are released!
|
// TODO: Update this mapping when new Flux minor versions are released!
|
||||||
// latestAPIVersions contains the latest API versions for each GroupKind
|
// latestAPIVersions contains the latest API versions for each GroupKind
|
||||||
// for each supported Flux version. We maintain the latest two minor versions.
|
// for each supported Flux version. The number of latest minor versions
|
||||||
|
// we maintain here must match what's documented here:
|
||||||
|
//
|
||||||
|
// https://fluxcd.io/flux/releases/#supported-releases
|
||||||
var latestAPIVersions = []APIVersions{
|
var latestAPIVersions = []APIVersions{
|
||||||
|
{
|
||||||
|
FluxVersion: "2.8",
|
||||||
|
LatestVersions: flux27LatestAPIVersions,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
FluxVersion: "2.7",
|
FluxVersion: "2.7",
|
||||||
LatestVersions: map[schema.GroupKind]string{
|
LatestVersions: flux27LatestAPIVersions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
FluxVersion: "2.6",
|
||||||
|
LatestVersions: flux26LatestAPIVersions,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var flux27LatestAPIVersions = map[schema.GroupKind]string{
|
||||||
// source-controller
|
// source-controller
|
||||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
@@ -92,11 +107,9 @@ var latestAPIVersions = []APIVersions{
|
|||||||
|
|
||||||
// source-watcher
|
// source-watcher
|
||||||
{Group: swv1b1.GroupVersion.Group, Kind: swv1b1.ArtifactGeneratorKind}: swv1b1.GroupVersion.Version,
|
{Group: swv1b1.GroupVersion.Group, Kind: swv1b1.ArtifactGeneratorKind}: swv1b1.GroupVersion.Version,
|
||||||
},
|
}
|
||||||
},
|
|
||||||
{
|
var flux26LatestAPIVersions = map[schema.GroupKind]string{
|
||||||
FluxVersion: "2.6",
|
|
||||||
LatestVersions: map[schema.GroupKind]string{
|
|
||||||
// source-controller
|
// source-controller
|
||||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.BucketKind}: sourcev1.GroupVersion.Version,
|
||||||
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
{Group: sourcev1.GroupVersion.Group, Kind: sourcev1.GitRepositoryKind}: sourcev1.GroupVersion.Version,
|
||||||
@@ -122,8 +135,6 @@ var latestAPIVersions = []APIVersions{
|
|||||||
|
|
||||||
// image-automation-controller
|
// image-automation-controller
|
||||||
{Group: imageautov1b2.GroupVersion.Group, Kind: imageautov1b2.ImageUpdateAutomationKind}: imageautov1b2.GroupVersion.Version,
|
{Group: imageautov1b2.GroupVersion.Group, Kind: imageautov1b2.ImageUpdateAutomationKind}: imageautov1b2.GroupVersion.Version,
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var migrateCmd = &cobra.Command{
|
var migrateCmd = &cobra.Command{
|
||||||
|
|||||||
118
cmd/flux/plugin.go
Normal file
118
cmd/flux/plugin.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/briandowns/spinner"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginHandler = plugin.NewHandler()
|
||||||
|
|
||||||
|
var pluginCmd = &cobra.Command{
|
||||||
|
Use: "plugin",
|
||||||
|
Short: "Manage Flux CLI plugins",
|
||||||
|
Long: `The plugin sub-commands manage Flux CLI plugins.`,
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// No-op: skip root's namespace DNS validation for plugin commands.
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(pluginCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// builtinCommandNames returns the names of all non-plugin commands on rootCmd.
|
||||||
|
func builtinCommandNames() []string {
|
||||||
|
var names []string
|
||||||
|
for _, c := range rootCmd.Commands() {
|
||||||
|
if c.GroupID != "plugin" {
|
||||||
|
names = append(names, c.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerPlugins scans the plugin directory and registers discovered
|
||||||
|
// plugins as Cobra subcommands on rootCmd.
|
||||||
|
func registerPlugins() {
|
||||||
|
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||||
|
if len(plugins) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rootCmd.ContainsGroup("plugin") {
|
||||||
|
rootCmd.AddGroup(&cobra.Group{
|
||||||
|
ID: "plugin",
|
||||||
|
Title: "Plugin Commands:",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range plugins {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: p.Name,
|
||||||
|
Short: fmt.Sprintf("Runs the %s plugin", p.Name),
|
||||||
|
Long: fmt.Sprintf("This command runs the %s plugin.\nUse 'flux %s --help' for full plugin help.", p.Name, p.Name),
|
||||||
|
DisableFlagParsing: true,
|
||||||
|
GroupID: "plugin",
|
||||||
|
ValidArgsFunction: plugin.CompleteFunc(p.Path),
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return plugin.Exec(p.Path, args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rootCmd.AddCommand(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNameVersion splits "operator@0.45.0" into ("operator", "0.45.0").
|
||||||
|
// If no @ is present, version is empty (latest).
|
||||||
|
func parseNameVersion(s string) (string, string) {
|
||||||
|
name, version, found := strings.Cut(s, "@")
|
||||||
|
if found {
|
||||||
|
return name, version
|
||||||
|
}
|
||||||
|
return s, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDigestRef reports whether ref is a content-addressable digest
|
||||||
|
// (e.g. "sha256:06e0a38...").
|
||||||
|
func isDigestRef(ref string) bool {
|
||||||
|
return strings.HasPrefix(ref, "sha256:")
|
||||||
|
}
|
||||||
|
|
||||||
|
// newCatalogClient creates a CatalogClient that respects FLUXCD_PLUGIN_CATALOG.
|
||||||
|
func newCatalogClient() *plugin.CatalogClient {
|
||||||
|
client := plugin.NewCatalogClient()
|
||||||
|
client.GetEnv = pluginHandler.GetEnv
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPluginSpinner(message string) *spinner.Spinner {
|
||||||
|
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
|
||||||
|
s.Suffix = " " + message
|
||||||
|
return s
|
||||||
|
}
|
||||||
96
cmd/flux/plugin_install.go
Normal file
96
cmd/flux/plugin_install.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginInstallCmd = &cobra.Command{
|
||||||
|
Use: "install <name>[@<version>|@<digest>]",
|
||||||
|
Short: "Install a plugin from the catalog",
|
||||||
|
Long: `The plugin install command downloads and installs a plugin from the Flux plugin catalog.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Install the latest version
|
||||||
|
flux plugin install operator
|
||||||
|
|
||||||
|
# Install a specific version
|
||||||
|
flux plugin install operator@0.45.0
|
||||||
|
|
||||||
|
# Install pinned to a specific digest
|
||||||
|
flux plugin install operator@sha256:06e0a38db4fa6bc9f705a577c7e58dc020bfe2618e45488599e5ef7bb62e3a8a`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: pluginInstallCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pluginCmd.AddCommand(pluginInstallCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginInstallCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
nameVersion := args[0]
|
||||||
|
name, ref := parseNameVersion(nameVersion)
|
||||||
|
|
||||||
|
catalogClient := newCatalogClient()
|
||||||
|
manifest, err := catalogClient.FetchManifest(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pv *plugintypes.Version
|
||||||
|
var plat *plugintypes.Platform
|
||||||
|
|
||||||
|
if isDigestRef(ref) {
|
||||||
|
dm, err := plugin.ResolveByDigest(manifest, ref, runtime.GOOS, runtime.GOARCH)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pv = dm.Version
|
||||||
|
plat = dm.Platform
|
||||||
|
} else {
|
||||||
|
pv, err = plugin.ResolveVersion(manifest, ref)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
plat, err = plugin.ResolvePlatform(pv, runtime.GOOS, runtime.GOARCH)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plugin %q v%s has no binary for %s/%s", name, pv.Version, runtime.GOOS, runtime.GOARCH)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginDir := pluginHandler.EnsurePluginDir()
|
||||||
|
|
||||||
|
installer := plugin.NewInstaller()
|
||||||
|
sp := newPluginSpinner(fmt.Sprintf("installing %s v%s", name, pv.Version))
|
||||||
|
sp.Start()
|
||||||
|
if err := installer.Install(pluginDir, manifest, pv, plat); err != nil {
|
||||||
|
sp.Stop()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sp.Stop()
|
||||||
|
|
||||||
|
logger.Successf("installed %s v%s", name, pv.Version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
57
cmd/flux/plugin_list.go
Normal file
57
cmd/flux/plugin_list.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
"github.com/fluxcd/flux2/v2/pkg/printers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Short: "List installed plugins",
|
||||||
|
Long: `The plugin list command shows all installed plugins with their versions and paths.`,
|
||||||
|
RunE: pluginListCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pluginCmd.AddCommand(pluginListCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginListCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
pluginDir := pluginHandler.PluginDir()
|
||||||
|
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||||
|
if len(plugins) == 0 {
|
||||||
|
cmd.Println("No plugins found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
header := []string{"NAME", "VERSION", "PATH"}
|
||||||
|
var rows [][]string
|
||||||
|
for _, p := range plugins {
|
||||||
|
version := "manual"
|
||||||
|
if receipt := plugin.ReadReceipt(pluginDir, p.Name); receipt != nil {
|
||||||
|
version = receipt.Version
|
||||||
|
}
|
||||||
|
rows = append(rows, []string{p.Name, version, p.Path})
|
||||||
|
}
|
||||||
|
|
||||||
|
return printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows)
|
||||||
|
}
|
||||||
81
cmd/flux/plugin_search.go
Normal file
81
cmd/flux/plugin_search.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
"github.com/fluxcd/flux2/v2/pkg/printers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginSearchCmd = &cobra.Command{
|
||||||
|
Use: "search [query]",
|
||||||
|
Short: "Search the plugin catalog",
|
||||||
|
Long: `The plugin search command lists available plugins from the Flux plugin catalog.`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: pluginSearchCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pluginCmd.AddCommand(pluginSearchCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginSearchCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
catalogClient := newCatalogClient()
|
||||||
|
catalog, err := catalogClient.FetchCatalog()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var query string
|
||||||
|
if len(args) == 1 {
|
||||||
|
query = strings.ToLower(args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginDir := pluginHandler.PluginDir()
|
||||||
|
header := []string{"NAME", "DESCRIPTION", "INSTALLED"}
|
||||||
|
var rows [][]string
|
||||||
|
for _, entry := range catalog.Plugins {
|
||||||
|
if query != "" {
|
||||||
|
if !strings.Contains(strings.ToLower(entry.Name), query) &&
|
||||||
|
!strings.Contains(strings.ToLower(entry.Description), query) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
installed := ""
|
||||||
|
if receipt := plugin.ReadReceipt(pluginDir, entry.Name); receipt != nil {
|
||||||
|
installed = receipt.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, []string{entry.Name, entry.Description, installed})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows) == 0 {
|
||||||
|
if query != "" {
|
||||||
|
cmd.Printf("No plugins matching %q found in catalog\n", query)
|
||||||
|
} else {
|
||||||
|
cmd.Println("No plugins found in catalog")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows)
|
||||||
|
}
|
||||||
286
cmd/flux/plugin_test.go
Normal file
286
cmd/flux/plugin_test.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPluginAppearsInHelp(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
fakeBin := pluginDir + "/flux-testplugin"
|
||||||
|
os.WriteFile(fakeBin, []byte("#!/bin/sh\necho test"), 0o755)
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: os.ReadDir,
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return pluginDir
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPlugins()
|
||||||
|
defer func() {
|
||||||
|
cmds := rootCmd.Commands()
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if cmd.Name() == "testplugin" {
|
||||||
|
rootCmd.RemoveCommand(cmd)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
output, err := executeCommand("--help")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(output, "Plugin Commands:") {
|
||||||
|
t.Error("expected 'Plugin Commands:' in help output")
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "testplugin") {
|
||||||
|
t.Error("expected 'testplugin' in help output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginListOutput(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
fakeBin := pluginDir + "/flux-myplugin"
|
||||||
|
os.WriteFile(fakeBin, []byte("#!/bin/sh\necho test"), 0o755)
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: os.ReadDir,
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return pluginDir
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeCommand("plugin list")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(output, "myplugin") {
|
||||||
|
t.Errorf("expected 'myplugin' in output, got: %s", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "manual") {
|
||||||
|
t.Errorf("expected 'manual' in output (no receipt), got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginListWithReceipt(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
fakeBin := pluginDir + "/flux-myplugin"
|
||||||
|
os.WriteFile(fakeBin, []byte("#!/bin/sh\necho test"), 0o755)
|
||||||
|
receipt := pluginDir + "/flux-myplugin.yaml"
|
||||||
|
os.WriteFile(receipt, []byte("name: myplugin\nversion: \"1.2.3\"\n"), 0o644)
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: os.ReadDir,
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return pluginDir
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeCommand("plugin list")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(output, "1.2.3") {
|
||||||
|
t.Errorf("expected version '1.2.3' in output, got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginListEmpty(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: os.ReadDir,
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return pluginDir
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := executeCommand("plugin list")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(output, "No plugins found") {
|
||||||
|
t.Errorf("expected 'No plugins found', got: %s", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoPluginsNoRegistration(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: func(name string) ([]os.DirEntry, error) {
|
||||||
|
return nil, fmt.Errorf("no dir")
|
||||||
|
},
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return "/nonexistent"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that registerPlugins with no plugins doesn't add any commands.
|
||||||
|
before := len(rootCmd.Commands())
|
||||||
|
registerPlugins()
|
||||||
|
after := len(rootCmd.Commands())
|
||||||
|
if after != before {
|
||||||
|
t.Errorf("expected no new commands, got %d new", after-before)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginSkipsPersistentPreRun(t *testing.T) {
|
||||||
|
// Plugin commands override root's PersistentPreRunE with a no-op,
|
||||||
|
// so an invalid namespace should not trigger a validation error.
|
||||||
|
_, err := executeCommand("plugin list")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("plugin list should not trigger root's namespace validation: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseNameVersion(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
wantName string
|
||||||
|
wantVersion string
|
||||||
|
}{
|
||||||
|
{"operator", "operator", ""},
|
||||||
|
{"operator@0.45.0", "operator", "0.45.0"},
|
||||||
|
{"my-tool@1.0.0", "my-tool", "1.0.0"},
|
||||||
|
{"plugin@", "plugin", ""},
|
||||||
|
{"operator@sha256:abc123", "operator", "sha256:abc123"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
name, version := parseNameVersion(tt.input)
|
||||||
|
if name != tt.wantName {
|
||||||
|
t.Errorf("name: got %q, want %q", name, tt.wantName)
|
||||||
|
}
|
||||||
|
if version != tt.wantVersion {
|
||||||
|
t.Errorf("version: got %q, want %q", version, tt.wantVersion)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsDigestRef(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"sha256:06e0a38db4fa6bc9f705a577c7e58dc020bfe2618e45488599e5ef7bb62e3a8a", true},
|
||||||
|
{"0.45.0", false},
|
||||||
|
{"", false},
|
||||||
|
{"sha256", false},
|
||||||
|
{"SHA256:abc", false}, // case-sensitive
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
if got := isDigestRef(tt.input); got != tt.want {
|
||||||
|
t.Errorf("isDigestRef(%q) = %v, want %v", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginDiscoverSkipsBuiltins(t *testing.T) {
|
||||||
|
origHandler := pluginHandler
|
||||||
|
defer func() { pluginHandler = origHandler }()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
for _, name := range []string{"flux-get", "flux-create", "flux-version"} {
|
||||||
|
os.WriteFile(pluginDir+"/"+name, []byte("#!/bin/sh"), 0o755)
|
||||||
|
}
|
||||||
|
os.WriteFile(pluginDir+"/flux-myplugin", []byte("#!/bin/sh"), 0o755)
|
||||||
|
|
||||||
|
pluginHandler = &plugin.Handler{
|
||||||
|
ReadDir: os.ReadDir,
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == "FLUXCD_PLUGINS" {
|
||||||
|
return pluginDir
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) { return t.TempDir(), nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||||
|
|
||||||
|
if len(plugins) != 1 {
|
||||||
|
names := make([]string, len(plugins))
|
||||||
|
for i, p := range plugins {
|
||||||
|
names[i] = p.Name
|
||||||
|
}
|
||||||
|
t.Fatalf("expected 1 plugin, got %d: %v", len(plugins), names)
|
||||||
|
}
|
||||||
|
if plugins[0].Name != "myplugin" {
|
||||||
|
t.Errorf("expected 'myplugin', got %q", plugins[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
48
cmd/flux/plugin_uninstall.go
Normal file
48
cmd/flux/plugin_uninstall.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginUninstallCmd = &cobra.Command{
|
||||||
|
Use: "uninstall <name>",
|
||||||
|
Aliases: []string{"delete"},
|
||||||
|
Short: "Uninstall a plugin",
|
||||||
|
Long: `The plugin uninstall command removes a plugin binary and its receipt from the plugin directory.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: pluginUninstallCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pluginCmd.AddCommand(pluginUninstallCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginUninstallCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
pluginDir := pluginHandler.PluginDir()
|
||||||
|
|
||||||
|
if err := plugin.Uninstall(pluginDir, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Successf("uninstalled %s", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
102
cmd/flux/plugin_update.go
Normal file
102
cmd/flux/plugin_update.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 The Flux authors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginUpdateCmd = &cobra.Command{
|
||||||
|
Use: "update [name]",
|
||||||
|
Aliases: []string{"upgrade"},
|
||||||
|
Short: "Update installed plugins",
|
||||||
|
Long: `The plugin update command updates installed plugins to their latest versions.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Update a single plugin
|
||||||
|
flux plugin update operator
|
||||||
|
|
||||||
|
# Update all installed plugins
|
||||||
|
flux plugin update`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: pluginUpdateCmdRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
pluginCmd.AddCommand(pluginUpdateCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pluginUpdateCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
|
catalogClient := newCatalogClient()
|
||||||
|
|
||||||
|
plugins := pluginHandler.Discover(builtinCommandNames())
|
||||||
|
if len(plugins) == 0 {
|
||||||
|
cmd.Println("No plugins found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a specific plugin is requested, filter to just that one.
|
||||||
|
if len(args) == 1 {
|
||||||
|
name := args[0]
|
||||||
|
var found bool
|
||||||
|
for _, p := range plugins {
|
||||||
|
if p.Name == name {
|
||||||
|
plugins = []plugin.Plugin{p}
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("plugin %q is not installed", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginDir := pluginHandler.EnsurePluginDir()
|
||||||
|
installer := plugin.NewInstaller()
|
||||||
|
for _, p := range plugins {
|
||||||
|
result := plugin.CheckUpdate(pluginDir, p.Name, catalogClient, runtime.GOOS, runtime.GOARCH)
|
||||||
|
if result.Err != nil {
|
||||||
|
logger.Failuref("error checking %s: %v", p.Name, result.Err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if result.Skipped {
|
||||||
|
if result.SkipReason == plugin.SkipReasonManual {
|
||||||
|
logger.Warningf("skipping %s (%s)", p.Name, result.SkipReason)
|
||||||
|
} else {
|
||||||
|
logger.Successf("%s already up to date (v%s)", p.Name, result.FromVersion)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sp := newPluginSpinner(fmt.Sprintf("updating %s v%s → v%s", p.Name, result.FromVersion, result.ToVersion))
|
||||||
|
sp.Start()
|
||||||
|
if err := installer.Install(pluginDir, result.Manifest, result.Version, result.Platform); err != nil {
|
||||||
|
sp.Stop()
|
||||||
|
logger.Failuref("error updating %s: %v", p.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sp.Stop()
|
||||||
|
logger.Successf("updated %s v%s → v%s", p.Name, result.FromVersion, result.ToVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -114,6 +114,7 @@ type pushArtifactFlags struct {
|
|||||||
debug bool
|
debug bool
|
||||||
reproducible bool
|
reproducible bool
|
||||||
insecure bool
|
insecure bool
|
||||||
|
resolveSymlinks bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var pushArtifactArgs = newPushArtifactFlags()
|
var pushArtifactArgs = newPushArtifactFlags()
|
||||||
@@ -137,6 +138,7 @@ func init() {
|
|||||||
pushArtifactCmd.Flags().BoolVarP(&pushArtifactArgs.debug, "debug", "", false, "display logs from underlying library")
|
pushArtifactCmd.Flags().BoolVarP(&pushArtifactArgs.debug, "debug", "", false, "display logs from underlying library")
|
||||||
pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.reproducible, "reproducible", false, "ensure reproducible image digests by setting the created timestamp to '1970-01-01T00:00:00Z'")
|
pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.reproducible, "reproducible", false, "ensure reproducible image digests by setting the created timestamp to '1970-01-01T00:00:00Z'")
|
||||||
pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.insecure, "insecure-registry", false, "allows artifacts to be pushed without TLS")
|
pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.insecure, "insecure-registry", false, "allows artifacts to be pushed without TLS")
|
||||||
|
pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.resolveSymlinks, "resolve-symlinks", false, "resolve symlinks by copying their targets into the artifact")
|
||||||
|
|
||||||
pushCmd.AddCommand(pushArtifactCmd)
|
pushCmd.AddCommand(pushArtifactCmd)
|
||||||
}
|
}
|
||||||
@@ -183,6 +185,15 @@ func pushArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("invalid path '%s', must point to an existing directory or file: %w", path, err)
|
return fmt.Errorf("invalid path '%s', must point to an existing directory or file: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pushArtifactArgs.resolveSymlinks {
|
||||||
|
resolved, cleanupDir, err := resolveSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolving symlinks failed: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(cleanupDir)
|
||||||
|
path = resolved
|
||||||
|
}
|
||||||
|
|
||||||
annotations := map[string]string{}
|
annotations := map[string]string{}
|
||||||
for _, annotation := range pushArtifactArgs.annotations {
|
for _, annotation := range pushArtifactArgs.annotations {
|
||||||
kv := strings.Split(annotation, "=")
|
kv := strings.Split(annotation, "=")
|
||||||
|
|||||||
@@ -152,7 +152,14 @@ func reconciliationHandled(kubeClient client.Client, namespacedName types.Namesp
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.Status == kstatus.CurrentStatus, nil
|
switch result.Status {
|
||||||
|
case kstatus.CurrentStatus:
|
||||||
|
return true, nil
|
||||||
|
case kstatus.InProgressStatus:
|
||||||
|
return false, nil
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("%s", result.Message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,17 @@ func (resume resumeCommand) run(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
resume.printMessage(reconcileResps)
|
resume.printMessage(reconcileResps)
|
||||||
|
|
||||||
|
// Return an error if any reconciliation failed
|
||||||
|
var failedCount int
|
||||||
|
for _, r := range reconcileResps {
|
||||||
|
if r.resumable != nil && r.err != nil {
|
||||||
|
failedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failedCount > 0 {
|
||||||
|
return fmt.Errorf("reconciliation failed for %d %s(s)", failedCount, resume.kind)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
cmd/flux/testdata/build-kustomization/configmaps/existing.yaml
vendored
Normal file
7
cmd/flux/testdata/build-kustomization/configmaps/existing.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: existing-config
|
||||||
|
namespace: default
|
||||||
|
data:
|
||||||
|
key: value
|
||||||
5
cmd/flux/testdata/build-kustomization/configmaps/kustomization.yaml
vendored
Normal file
5
cmd/flux/testdata/build-kustomization/configmaps/kustomization.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- ./existing.yaml
|
||||||
|
- ./new.yaml
|
||||||
7
cmd/flux/testdata/build-kustomization/configmaps/new.yaml
vendored
Normal file
7
cmd/flux/testdata/build-kustomization/configmaps/new.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: new-config
|
||||||
|
namespace: default
|
||||||
|
data:
|
||||||
|
key: value
|
||||||
7
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/configmap.yaml
vendored
Normal file
7
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/configmap.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: app-config
|
||||||
|
namespace: new-ns
|
||||||
|
data:
|
||||||
|
key: value
|
||||||
5
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/kustomization.yaml
vendored
Normal file
5
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/kustomization.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- ./namespace.yaml
|
||||||
|
- ./configmap.yaml
|
||||||
4
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/namespace.yaml
vendored
Normal file
4
cmd/flux/testdata/build-kustomization/new-namespace-and-configmap/namespace.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: new-ns
|
||||||
4
cmd/flux/testdata/build-kustomization/new-namespace-only/kustomization.yaml
vendored
Normal file
4
cmd/flux/testdata/build-kustomization/new-namespace-only/kustomization.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- ./namespace.yaml
|
||||||
4
cmd/flux/testdata/build-kustomization/new-namespace-only/namespace.yaml
vendored
Normal file
4
cmd/flux/testdata/build-kustomization/new-namespace-only/namespace.yaml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: new-ns
|
||||||
2
cmd/flux/testdata/check/check_pre.golden
vendored
2
cmd/flux/testdata/check/check_pre.golden
vendored
@@ -1,3 +1,3 @@
|
|||||||
► checking prerequisites
|
► checking prerequisites
|
||||||
✔ Kubernetes {{ .serverVersion }} >=1.32.0-0
|
✔ Kubernetes {{ .serverVersion }} >=1.33.0-0
|
||||||
✔ prerequisites checks passed
|
✔ prerequisites checks passed
|
||||||
|
|||||||
13
cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml
vendored
Normal file
13
cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr-audience.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4
|
||||||
|
name: gcr-secret
|
||||||
|
namespace: my-namespace
|
||||||
|
stringData:
|
||||||
|
audience: https://custom.audience.example.com
|
||||||
|
email: sa@project.iam.gserviceaccount.com
|
||||||
|
token: test-token
|
||||||
|
|
||||||
13
cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr.yaml
vendored
Normal file
13
cmd/flux/testdata/create_secret/receiver/secret-receiver-gcr.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4
|
||||||
|
name: gcr-secret
|
||||||
|
namespace: my-namespace
|
||||||
|
stringData:
|
||||||
|
audience: https://flux.example.com/hook/6d6c55e9affb9d1e0d101ce604ae4270880ec1ff24d1bd2d928fcd64243d21a4
|
||||||
|
email: sa@project.iam.gserviceaccount.com
|
||||||
|
token: test-token
|
||||||
|
|
||||||
11
cmd/flux/testdata/create_secret/receiver/secret-receiver.yaml
vendored
Normal file
11
cmd/flux/testdata/create_secret/receiver/secret-receiver.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
notification.toolkit.fluxcd.io/webhook: https://flux.example.com/hook/106120121d366c2f67e93200f6c1dbe938235eb588daa5e8c0516d3a77ac1dee
|
||||||
|
name: receiver-secret
|
||||||
|
namespace: my-namespace
|
||||||
|
stringData:
|
||||||
|
token: test-token
|
||||||
|
|
||||||
1
cmd/flux/testdata/diff-kustomization/diff-new-namespace-only.golden
vendored
Normal file
1
cmd/flux/testdata/diff-kustomization/diff-new-namespace-only.golden
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
► Namespace/new-ns created
|
||||||
9
cmd/flux/testdata/diff-kustomization/diff-taking-ownership.golden
vendored
Normal file
9
cmd/flux/testdata/diff-kustomization/diff-taking-ownership.golden
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
► ConfigMap/default/existing-config drifted
|
||||||
|
|
||||||
|
metadata
|
||||||
|
+ one map entry added:
|
||||||
|
labels:
|
||||||
|
kustomize.toolkit.fluxcd.io/name: configmaps
|
||||||
|
kustomize.toolkit.fluxcd.io/namespace:
|
||||||
|
|
||||||
|
► ConfigMap/default/new-config created
|
||||||
7
cmd/flux/testdata/diff-kustomization/existing-configmap.yaml
vendored
Normal file
7
cmd/flux/testdata/diff-kustomization/existing-configmap.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: existing-config
|
||||||
|
namespace: default
|
||||||
|
data:
|
||||||
|
key: value
|
||||||
14
cmd/flux/testdata/diff-kustomization/flux-kustomization-configmaps.yaml
vendored
Normal file
14
cmd/flux/testdata/diff-kustomization/flux-kustomization-configmaps.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: configmaps
|
||||||
|
spec:
|
||||||
|
interval: 5m0s
|
||||||
|
path: ./kustomize
|
||||||
|
force: true
|
||||||
|
prune: true
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: configmaps
|
||||||
|
targetNamespace: default
|
||||||
14
cmd/flux/testdata/diff-kustomization/flux-kustomization-local-only.yaml
vendored
Normal file
14
cmd/flux/testdata/diff-kustomization/flux-kustomization-local-only.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: podinfo
|
||||||
|
spec:
|
||||||
|
interval: 5m0s
|
||||||
|
path: ./kustomize
|
||||||
|
force: true
|
||||||
|
prune: true
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: podinfo
|
||||||
|
targetNamespace: default
|
||||||
13
cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml
vendored
Normal file
13
cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-and-configmap.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: new-namespace-and-configmap
|
||||||
|
spec:
|
||||||
|
interval: 5m0s
|
||||||
|
path: ./kustomize
|
||||||
|
force: true
|
||||||
|
prune: true
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: new-namespace-and-configmap
|
||||||
13
cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml
vendored
Normal file
13
cmd/flux/testdata/diff-kustomization/flux-kustomization-new-namespace-only.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||||
|
kind: Kustomization
|
||||||
|
metadata:
|
||||||
|
name: new-namespace-only
|
||||||
|
spec:
|
||||||
|
interval: 5m0s
|
||||||
|
path: ./kustomize
|
||||||
|
force: true
|
||||||
|
prune: true
|
||||||
|
sourceRef:
|
||||||
|
kind: GitRepository
|
||||||
|
name: new-namespace-only
|
||||||
@@ -26,6 +26,8 @@ The following template can be used for the GitHub release page:
|
|||||||
|
|
||||||
<!-- Text describing the most important changes in this release -->
|
<!-- Text describing the most important changes in this release -->
|
||||||
|
|
||||||
|
ℹ️ Please follow the [Upgrade Procedure for Flux v2.7+](https://github.com/fluxcd/flux2/discussions/5572) for a smooth upgrade from Flux v2.6 to the latest version.
|
||||||
|
|
||||||
### Fixes and improvements
|
### Fixes and improvements
|
||||||
|
|
||||||
<!-- List of fixes and improvements to the controllers and CLI -->
|
<!-- List of fixes and improvements to the controllers and CLI -->
|
||||||
@@ -36,7 +38,7 @@ The following template can be used for the GitHub release page:
|
|||||||
|
|
||||||
## Components changelog
|
## Components changelog
|
||||||
|
|
||||||
- <name>-controller [v<version>](https://github.com/fluxcd/<name>-controller/blob/<version>/CHANGELOG.md
|
- <name>-controller [v<version>](https://github.com/fluxcd/<name>-controller/blob/<version>/CHANGELOG.md)
|
||||||
|
|
||||||
## CLI changelog
|
## CLI changelog
|
||||||
|
|
||||||
|
|||||||
175
go.mod
175
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/fluxcd/flux2/v2
|
module github.com/fluxcd/flux2/v2
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26.0
|
||||||
|
|
||||||
// Fix CVE-2022-28948.
|
// Fix CVE-2022-28948.
|
||||||
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
|
replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -8,60 +8,61 @@ replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1
|
|||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.4.0
|
github.com/Masterminds/semver/v3 v3.4.0
|
||||||
github.com/ProtonMail/go-crypto v1.3.0
|
github.com/ProtonMail/go-crypto v1.3.0
|
||||||
|
github.com/briandowns/spinner v1.23.2
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1
|
github.com/cyphar/filepath-securejoin v0.6.1
|
||||||
github.com/distribution/distribution/v3 v3.0.0
|
github.com/distribution/distribution/v3 v3.1.0
|
||||||
github.com/fluxcd/cli-utils v0.37.1-flux.1
|
github.com/fluxcd/cli-utils v0.37.2-flux.1
|
||||||
github.com/fluxcd/go-git-providers v0.25.0
|
github.com/fluxcd/go-git-providers v0.26.0
|
||||||
github.com/fluxcd/helm-controller/api v1.4.5
|
github.com/fluxcd/helm-controller/api v1.5.4
|
||||||
github.com/fluxcd/image-automation-controller/api v1.0.4
|
github.com/fluxcd/image-automation-controller/api v1.1.2
|
||||||
github.com/fluxcd/image-reflector-controller/api v1.0.4
|
github.com/fluxcd/image-reflector-controller/api v1.1.1
|
||||||
github.com/fluxcd/kustomize-controller/api v1.7.3
|
github.com/fluxcd/kustomize-controller/api v1.8.4
|
||||||
github.com/fluxcd/notification-controller/api v1.7.5
|
github.com/fluxcd/notification-controller/api v1.8.4
|
||||||
github.com/fluxcd/pkg/apis/event v0.22.0
|
github.com/fluxcd/pkg/apis/event v0.25.0
|
||||||
github.com/fluxcd/pkg/apis/meta v1.25.0
|
github.com/fluxcd/pkg/apis/meta v1.26.0
|
||||||
github.com/fluxcd/pkg/auth v0.36.0
|
github.com/fluxcd/pkg/auth v0.40.0
|
||||||
github.com/fluxcd/pkg/chartutil v1.21.0
|
github.com/fluxcd/pkg/chartutil v1.23.0
|
||||||
github.com/fluxcd/pkg/envsubst v1.5.0
|
github.com/fluxcd/pkg/envsubst v1.5.0
|
||||||
github.com/fluxcd/pkg/git v0.41.0
|
github.com/fluxcd/pkg/git v0.46.0
|
||||||
github.com/fluxcd/pkg/kustomize v1.25.0
|
github.com/fluxcd/pkg/kustomize v1.29.0
|
||||||
github.com/fluxcd/pkg/oci v0.59.0
|
github.com/fluxcd/pkg/oci v0.63.0
|
||||||
github.com/fluxcd/pkg/runtime v0.96.0
|
github.com/fluxcd/pkg/runtime v0.103.0
|
||||||
github.com/fluxcd/pkg/sourceignore v0.16.0
|
github.com/fluxcd/pkg/sourceignore v0.17.0
|
||||||
github.com/fluxcd/pkg/ssa v0.64.0
|
github.com/fluxcd/pkg/ssa v0.70.0
|
||||||
github.com/fluxcd/pkg/ssh v0.24.0
|
github.com/fluxcd/pkg/ssh v0.24.0
|
||||||
github.com/fluxcd/pkg/tar v0.17.0
|
github.com/fluxcd/pkg/tar v0.17.0
|
||||||
github.com/fluxcd/pkg/version v0.12.0
|
github.com/fluxcd/pkg/version v0.14.0
|
||||||
github.com/fluxcd/source-controller/api v1.7.4
|
github.com/fluxcd/source-controller/api v1.8.3
|
||||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3
|
github.com/fluxcd/source-watcher/api/v2 v2.1.1
|
||||||
github.com/go-git/go-git/v5 v5.16.4
|
github.com/go-git/go-git/v5 v5.18.0
|
||||||
github.com/go-logr/logr v1.4.3
|
github.com/go-logr/logr v1.4.3
|
||||||
github.com/gonvenience/bunt v1.4.2
|
github.com/gonvenience/bunt v1.4.2
|
||||||
github.com/gonvenience/ytbx v1.4.7
|
github.com/gonvenience/ytbx v1.4.7
|
||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/google/go-containerregistry v0.20.7
|
github.com/google/go-containerregistry v0.20.7
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||||
|
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||||
github.com/homeport/dyff v1.10.2
|
github.com/homeport/dyff v1.10.2
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||||
github.com/manifoldco/promptui v0.9.0
|
github.com/manifoldco/promptui v0.9.0
|
||||||
github.com/mattn/go-shellwords v1.0.12
|
github.com/mattn/go-shellwords v1.0.12
|
||||||
github.com/notaryproject/notation-go v1.3.2
|
github.com/notaryproject/notation-go v1.3.2
|
||||||
github.com/olekukonko/tablewriter v0.0.5
|
github.com/olekukonko/tablewriter v0.0.5
|
||||||
github.com/onsi/gomega v1.39.0
|
github.com/onsi/gomega v1.39.1
|
||||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
|
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/theckman/yacspin v0.13.12
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/crypto v0.47.0
|
golang.org/x/term v0.42.0
|
||||||
golang.org/x/term v0.39.0
|
golang.org/x/text v0.36.0
|
||||||
golang.org/x/text v0.33.0
|
k8s.io/api v0.35.2
|
||||||
k8s.io/api v0.35.0
|
k8s.io/apiextensions-apiserver v0.35.2
|
||||||
k8s.io/apiextensions-apiserver v0.35.0
|
k8s.io/apimachinery v0.35.2
|
||||||
k8s.io/apimachinery v0.35.0
|
k8s.io/cli-runtime v0.35.2
|
||||||
k8s.io/cli-runtime v0.35.0
|
k8s.io/client-go v0.35.2
|
||||||
k8s.io/client-go v0.35.0
|
k8s.io/kubectl v0.35.2
|
||||||
k8s.io/kubectl v0.35.0
|
sigs.k8s.io/controller-runtime v0.23.3
|
||||||
sigs.k8s.io/controller-runtime v0.23.0
|
sigs.k8s.io/kustomize/api v0.21.1
|
||||||
sigs.k8s.io/kustomize/api v0.21.0
|
sigs.k8s.io/kustomize/kyaml v0.21.1
|
||||||
sigs.k8s.io/kustomize/kyaml v0.21.0
|
|
||||||
sigs.k8s.io/yaml v1.6.0
|
sigs.k8s.io/yaml v1.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ require (
|
|||||||
cloud.google.com/go/auth v0.18.0 // indirect
|
cloud.google.com/go/auth v0.18.0 // indirect
|
||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
code.gitea.io/sdk/gitea v0.22.0 // indirect
|
code.gitea.io/sdk/gitea v0.23.2 // indirect
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
github.com/42wim/httpsig v1.2.3 // indirect
|
github.com/42wim/httpsig v1.2.3 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||||
@@ -102,12 +103,12 @@ require (
|
|||||||
github.com/aws/smithy-go v1.24.0 // indirect
|
github.com/aws/smithy-go v1.24.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||||
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect
|
github.com/bshuster-repo/logrus-logstash-hook v1.1.0 // indirect
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||||
github.com/chzyer/readline v1.5.1 // indirect
|
github.com/chzyer/readline v1.5.1 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
|
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||||
@@ -115,10 +116,10 @@ require (
|
|||||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/cli v29.1.5+incompatible // indirect
|
github.com/docker/cli v29.2.0+incompatible // indirect
|
||||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
github.com/docker/go-events v0.0.0-20250808211157-605354379745 // indirect
|
||||||
github.com/docker/go-metrics v0.0.1 // indirect
|
github.com/docker/go-metrics v0.0.1 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
@@ -128,7 +129,7 @@ require (
|
|||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect
|
github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect
|
||||||
github.com/fluxcd/pkg/apis/kustomize v1.15.0 // indirect
|
github.com/fluxcd/pkg/apis/kustomize v1.16.0 // indirect
|
||||||
github.com/fluxcd/pkg/cache v0.13.0 // indirect
|
github.com/fluxcd/pkg/cache v0.13.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
@@ -136,7 +137,7 @@ require (
|
|||||||
github.com/go-errors/errors v1.5.1 // indirect
|
github.com/go-errors/errors v1.5.1 // indirect
|
||||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.7.0 // indirect
|
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10 // indirect
|
github.com/go-ldap/ldap/v3 v3.4.10 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||||
@@ -150,20 +151,18 @@ require (
|
|||||||
github.com/gonvenience/text v1.0.9 // indirect
|
github.com/gonvenience/text v1.0.9 // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/gnostic-models v0.7.0 // indirect
|
github.com/google/gnostic-models v0.7.0 // indirect
|
||||||
github.com/google/go-github/v75 v75.0.0 // indirect
|
github.com/google/go-github/v82 v82.0.0 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.2.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
|
||||||
github.com/gorilla/handlers v1.5.2 // indirect
|
github.com/gorilla/handlers v1.5.2 // indirect
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
|
||||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
|
||||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||||
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect
|
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
|
||||||
@@ -172,7 +171,7 @@ require (
|
|||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.1 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||||
github.com/mailru/easyjson v0.9.0 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
@@ -199,9 +198,9 @@ require (
|
|||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.66.1 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/otlptranslator v0.0.2 // indirect
|
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||||
github.com/prometheus/procfs v0.17.0 // indirect
|
github.com/prometheus/procfs v0.20.1 // indirect
|
||||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
|
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
|
||||||
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect
|
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.7.3 // indirect
|
github.com/redis/go-redis/v9 v9.7.3 // indirect
|
||||||
@@ -217,54 +216,54 @@ require (
|
|||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/xlab/treeprint v1.2.0 // indirect
|
github.com/xlab/treeprint v1.2.0 // indirect
|
||||||
gitlab.com/gitlab-org/api/client-go v0.142.5 // indirect
|
gitlab.com/gitlab-org/api/client-go v1.29.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 // indirect
|
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 // indirect
|
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect
|
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/log v0.14.0 // indirect
|
go.opentelemetry.io/otel/log v0.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0 // indirect
|
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.53.0 // indirect
|
||||||
golang.org/x/oauth2 v0.34.0 // indirect
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||||
google.golang.org/api v0.261.0 // indirect
|
google.golang.org/api v0.261.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
google.golang.org/grpc v1.78.0 // indirect
|
google.golang.org/grpc v1.80.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
helm.sh/helm/v4 v4.1.0 // indirect
|
helm.sh/helm/v4 v4.1.4 // indirect
|
||||||
k8s.io/component-base v0.35.0 // indirect
|
k8s.io/component-base v0.35.2 // indirect
|
||||||
k8s.io/klog/v2 v2.130.1 // indirect
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
381
go.sum
381
go.sum
@@ -4,8 +4,8 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
|
|||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
|
code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg=
|
||||||
code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||||
@@ -47,6 +47,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
|||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
|
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||||
|
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
@@ -91,8 +93,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
|||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||||
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
|
github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w=
|
||||||
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
|
||||||
|
github.com/bshuster-repo/logrus-logstash-hook v1.1.0 h1:o2FzZifLg+z/DN1OFmzTWzZZx/roaqt8IPZCIVco8r4=
|
||||||
|
github.com/bshuster-repo/logrus-logstash-hook v1.1.0/go.mod h1:Q2aXOe7rNuPgbBtPCOzYyWDvKX7+FpxE5sRdvcPoui0=
|
||||||
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
@@ -115,8 +119,8 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
|
|||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=
|
github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=
|
github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=
|
||||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||||
@@ -138,18 +142,18 @@ github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454Wv
|
|||||||
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM=
|
github.com/distribution/distribution/v3 v3.1.0 h1:u1v788HreKTLGdNY6s7px8Exgrs9mZ9UrCDjSrpCM8g=
|
||||||
github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU=
|
github.com/distribution/distribution/v3 v3.1.0/go.mod h1:73BuF5/ziMHNVt7nnL1roYpH4Eg/FgUlKZm3WryIx/o=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
|
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||||
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
|
||||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
|
github.com/docker/go-events v0.0.0-20250808211157-605354379745 h1:yOn6Ze6IbYI/KAw2lw/83ELYvZh6hvsygTVkD0dzMC4=
|
||||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
github.com/docker/go-events v0.0.0-20250808211157-605354379745/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
||||||
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
|
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
|
||||||
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
|
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
|
||||||
github.com/elazarl/goproxy v1.8.0 h1:dt561rX7UAYMeFRLtzFx6uQGl2TpL1dr6uCG23nFQSY=
|
github.com/elazarl/goproxy v1.8.0 h1:dt561rX7UAYMeFRLtzFx6uQGl2TpL1dr6uCG23nFQSY=
|
||||||
@@ -168,62 +172,62 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
|||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fluxcd/cli-utils v0.37.1-flux.1 h1:WnG2mHxCPZMj/soIq/S/1zvbrGCJN3GJGbNfG06X55M=
|
github.com/fluxcd/cli-utils v0.37.2-flux.1 h1:tQ588ghtRN+E+kHq415FddfqA9v4brn/1WWgrP6rQR0=
|
||||||
github.com/fluxcd/cli-utils v0.37.1-flux.1/go.mod h1:aND5wX3LuTFtB7eUT7vsWr8mmxRVSPR2Wkvbn0SqPfw=
|
github.com/fluxcd/cli-utils v0.37.2-flux.1/go.mod h1:LcWSu1NYET8d8U7O326RhEm5JkQXCMK6ITu4G1CT02c=
|
||||||
github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg=
|
github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg=
|
||||||
github.com/fluxcd/gitkit v0.6.0/go.mod h1:svOHuKi0fO9HoawdK4HfHAJJseZDHHjk7I3ihnCIqNo=
|
github.com/fluxcd/gitkit v0.6.0/go.mod h1:svOHuKi0fO9HoawdK4HfHAJJseZDHHjk7I3ihnCIqNo=
|
||||||
github.com/fluxcd/go-git-providers v0.25.0 h1:zkVgujjo2VjKXbucrlTyNhHd9x+27oqyghJX9uLwQv4=
|
github.com/fluxcd/go-git-providers v0.26.0 h1:0DUsXc1nS9Fe4n8tXSEUCGemWzHShd66gmotayDPekw=
|
||||||
github.com/fluxcd/go-git-providers v0.25.0/go.mod h1:8Mx5WRYb61FIjOA26DAi4Ls2rZUHSxP8Nl9qkQHDch8=
|
github.com/fluxcd/go-git-providers v0.26.0/go.mod h1:VJDKUOhZwNAIqDF5iPtIpTr/annsDbKMkPpWiDMBdpo=
|
||||||
github.com/fluxcd/helm-controller/api v1.4.5 h1:hMEBtgXUbJjp+ah0jPI3OOQNVngoToOQvTgFgVpAjNg=
|
github.com/fluxcd/helm-controller/api v1.5.4 h1:wbAwD+cSGBZEhT3qq1naBKkitdNbqRtWQUFNA3XTXOc=
|
||||||
github.com/fluxcd/helm-controller/api v1.4.5/go.mod h1:rCgx3qhjjtoIH+1EbzFC2vN71/pp0PgMDrZnGCZX5XY=
|
github.com/fluxcd/helm-controller/api v1.5.4/go.mod h1:lTgeUmtVYExMKp7mRDncsr4JwHTz3LFtLjRJZeR98lI=
|
||||||
github.com/fluxcd/image-automation-controller/api v1.0.4 h1:Fgdy97hXkyh/JFjxLIyq4ZDHsKsa49aumtrvIyjVd08=
|
github.com/fluxcd/image-automation-controller/api v1.1.2 h1:Maa4qycz+iBCN/yJv9xNvuuj2IYihDAsSVwpHLhxkuk=
|
||||||
github.com/fluxcd/image-automation-controller/api v1.0.4/go.mod h1:LLBf4XQJAgnpIMlZUwfpVIkCdUtBOi31B6fDbPwBCq4=
|
github.com/fluxcd/image-automation-controller/api v1.1.2/go.mod h1:lkD/drkD6Wc+2SDjVj5KqfozEucTLFexWgby/5ft660=
|
||||||
github.com/fluxcd/image-reflector-controller/api v1.0.4 h1:/JGpTZf4eMcKG2FpWfP5H7SneSrD5P8EvwGnHiH/WLY=
|
github.com/fluxcd/image-reflector-controller/api v1.1.1 h1:4Bj1abzVnjj8+b/293kNeFMRJc+y2wO8Z12ReZ/gA0w=
|
||||||
github.com/fluxcd/image-reflector-controller/api v1.0.4/go.mod h1:5GS4ojHaz+W6hK80WakGIOYk8sn93AyV5X+YOne1XMw=
|
github.com/fluxcd/image-reflector-controller/api v1.1.1/go.mod h1:j4JSIocL42HQ77Veg1t60sApOy+lng8/cbXHXGSnfi0=
|
||||||
github.com/fluxcd/kustomize-controller/api v1.7.3 h1:g+C9Il+H33DQi/ZiQ8KpTvL9KXebXnS4oM/0uJ/C8Gw=
|
github.com/fluxcd/kustomize-controller/api v1.8.4 h1:13+9CgxGj67Bn7wzFILw8S4r4urEgiLTODwjA4XU0mU=
|
||||||
github.com/fluxcd/kustomize-controller/api v1.7.3/go.mod h1:Yj80JyfQpBUgLhsUZ/c86qcvPGO2+P1VCKsb8fL+L/k=
|
github.com/fluxcd/kustomize-controller/api v1.8.4/go.mod h1:c/mUPIffDDLg1EicXCJtX4N/rc+z5Zh0e/CXjhd7Dyc=
|
||||||
github.com/fluxcd/notification-controller/api v1.7.5 h1:6CO5bKyjodiK9exQFOdBcz0XLeo17rrrWQBTJL9NNa8=
|
github.com/fluxcd/notification-controller/api v1.8.4 h1:KhHHVhQNtQsY+cVm/Y/8vhhFfrEOxM2AL/8JF8LAjMg=
|
||||||
github.com/fluxcd/notification-controller/api v1.7.5/go.mod h1:IciwSg8Q0pVtdbsyDyEXx/MxBKWeagxAazpm64C8oCE=
|
github.com/fluxcd/notification-controller/api v1.8.4/go.mod h1:ozgJGQPy0dG5eOsLZlwAr6n0q/y6+TWd1fGOtavlXJA=
|
||||||
github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA=
|
github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA=
|
||||||
github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4=
|
github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4=
|
||||||
github.com/fluxcd/pkg/apis/event v0.22.0 h1:nCW0TnneMnscSnj9NlaSKcvyC+436MbY1GyKn/4YnII=
|
github.com/fluxcd/pkg/apis/event v0.25.0 h1:zdwytvDhG+fk+Ywl5DOtv7TklkrVgM21WHm1f+YhleE=
|
||||||
github.com/fluxcd/pkg/apis/event v0.22.0/go.mod h1:Hoi4DejaNKVahGkRXqGBjT9h1aKmhc7RCYcsgoTieqc=
|
github.com/fluxcd/pkg/apis/event v0.25.0/go.mod h1:TlK8HWYrTwl0raqBRC+ROoNpYW5fdVnwcwOBOx5Kzw8=
|
||||||
github.com/fluxcd/pkg/apis/kustomize v1.15.0 h1:p8wPIxdmn0vy0a664rsE9JKCfnliZz4HUsDcTy4ZOxA=
|
github.com/fluxcd/pkg/apis/kustomize v1.16.0 h1:PhWXEhqQqsisIpwp1/wHvTvo+MO+GGzsBPoN0ZnRE3Y=
|
||||||
github.com/fluxcd/pkg/apis/kustomize v1.15.0/go.mod h1:XWdsx8P15OiMaQIvmUjYWdmD3zAwhl5q9osl5iCqcOk=
|
github.com/fluxcd/pkg/apis/kustomize v1.16.0/go.mod h1:IZOy4CCtR/hxMGb7erK1RfbGnczVv4/dRBoVD37AywI=
|
||||||
github.com/fluxcd/pkg/apis/meta v1.25.0 h1:fmZgMoe7yITGfhFqdOs7w2GOu3Y/2Vvz4+4p/eay3eA=
|
github.com/fluxcd/pkg/apis/meta v1.26.0 h1:dxP1FfBpTCYso6odzRcltVnnRuBb2VyhhgV0VX9YbUE=
|
||||||
github.com/fluxcd/pkg/apis/meta v1.25.0/go.mod h1:1D92RqAet0/n/cH5S0khBXweirHWkw9rCO0V4NCY6xc=
|
github.com/fluxcd/pkg/apis/meta v1.26.0/go.mod h1:c7o6mJGLCMvNrfdinGZehkrdZuFT9vZdZNrn66DtVD0=
|
||||||
github.com/fluxcd/pkg/auth v0.36.0 h1:4T61EOyRAElhJedwglfa68OxsD6GiNPGGTMZIeYE3sM=
|
github.com/fluxcd/pkg/auth v0.40.0 h1:p6Kw6KH+z8oRqngKhmTt8ILKD/rC+8tP87a//kLZhi8=
|
||||||
github.com/fluxcd/pkg/auth v0.36.0/go.mod h1:pRet9dmeOW3iHEh9BwCvhvjEQ5HjQLi4lblaIfR/yJg=
|
github.com/fluxcd/pkg/auth v0.40.0/go.mod h1:Oq/hIEKUMTbL2bv5blf+EhC/jXXJLsOjIMtJj/AtG3Y=
|
||||||
github.com/fluxcd/pkg/cache v0.13.0 h1:MqtlgOwIVcGKKgV422e39O+KFSVMWuExKeRaMDBjJlk=
|
github.com/fluxcd/pkg/cache v0.13.0 h1:MqtlgOwIVcGKKgV422e39O+KFSVMWuExKeRaMDBjJlk=
|
||||||
github.com/fluxcd/pkg/cache v0.13.0/go.mod h1:0xRZ1hitrIFQ6pl68ke2wZLbIqA2VLzY78HpDo9DVxs=
|
github.com/fluxcd/pkg/cache v0.13.0/go.mod h1:0xRZ1hitrIFQ6pl68ke2wZLbIqA2VLzY78HpDo9DVxs=
|
||||||
github.com/fluxcd/pkg/chartutil v1.21.0 h1:NJYhlekwBwuqMpRgsOlcsJrw2Xq0cBJW0Nmvz2oMluA=
|
github.com/fluxcd/pkg/chartutil v1.23.0 h1:ohstQEVnrBIbN85FGu83hnmAohLl0PdOoPlsM6+cjyI=
|
||||||
github.com/fluxcd/pkg/chartutil v1.21.0/go.mod h1:Gv50bF3SS4OvvKCyyIMRkGeNzZk6Fsh4+lAdrjx97T4=
|
github.com/fluxcd/pkg/chartutil v1.23.0/go.mod h1:kFhmD6DwBgRsvC1ilINsomargMi2WbqvSndWQLikkLc=
|
||||||
github.com/fluxcd/pkg/envsubst v1.5.0 h1:S07mo+MkGhptdHA4pRze5HPKlc8tHxKswNdcMZi1WDY=
|
github.com/fluxcd/pkg/envsubst v1.5.0 h1:S07mo+MkGhptdHA4pRze5HPKlc8tHxKswNdcMZi1WDY=
|
||||||
github.com/fluxcd/pkg/envsubst v1.5.0/go.mod h1:c3a8DYI855sZUubHFYQbjfjop6Wu4/zg1cLyf7SnCes=
|
github.com/fluxcd/pkg/envsubst v1.5.0/go.mod h1:c3a8DYI855sZUubHFYQbjfjop6Wu4/zg1cLyf7SnCes=
|
||||||
github.com/fluxcd/pkg/git v0.41.0 h1:WvvIUFssFDKpRrptJjDf0B4mrUCwhesv1Txu3DzTsl8=
|
github.com/fluxcd/pkg/git v0.46.0 h1:QMh0+ZzQ2jO6rIGj4ffR5trZ8g/cxvt8cVajReJ8Iyw=
|
||||||
github.com/fluxcd/pkg/git v0.41.0/go.mod h1:iqR4eZEhd3gdRSkv+VDP3Qz9WCner3aZ5ClkOUe+3fc=
|
github.com/fluxcd/pkg/git v0.46.0/go.mod h1:iHcIjx9c8zye3PQiajTJYxgOMRiy7WCs+hfLKDswpfI=
|
||||||
github.com/fluxcd/pkg/gittestserver v0.24.0 h1:ZIksyENX8yPlB95GJGoUIT171o2oKFJvFSXu+4mEmzU=
|
github.com/fluxcd/pkg/gittestserver v0.26.0 h1:+RZrCzFRsE+d5WaqAoqaPCEgcgv/jZp6+f7DS0+Ynb8=
|
||||||
github.com/fluxcd/pkg/gittestserver v0.24.0/go.mod h1:9l+gwEfqqe/WxiRvIrQxircgDcXUF3/tw/1Bie/XwJc=
|
github.com/fluxcd/pkg/gittestserver v0.26.0/go.mod h1:7fybYb0yej1fFNiF1ohs0Jr0XzyaZQ/cRh3AFEoCtuc=
|
||||||
github.com/fluxcd/pkg/kustomize v1.25.0 h1:0jjACHxaMif+RYwrlDDqA09vRtib7WbqU8MmF0k91bM=
|
github.com/fluxcd/pkg/kustomize v1.29.0 h1:B/5hr9wX6INwaQAZ6BGKVNvZm++A6qjgorUfoaBAwPw=
|
||||||
github.com/fluxcd/pkg/kustomize v1.25.0/go.mod h1:253Y78WyQJ+cD1krdoysluy9bsm5yee6SdmA4xf1hnk=
|
github.com/fluxcd/pkg/kustomize v1.29.0/go.mod h1:cW08mnngSP8MJYb6mDmMvxH8YjNATdiML0udb37dk+M=
|
||||||
github.com/fluxcd/pkg/oci v0.59.0 h1:0b+iy52QEjGE5vZzmlqjlcTTUYtNZ3F70yG6cyKR+Mg=
|
github.com/fluxcd/pkg/oci v0.63.0 h1:ZPKTT2C+gWYjhP63xC76iTPdYE9w3ABcsDq77uhAgwo=
|
||||||
github.com/fluxcd/pkg/oci v0.59.0/go.mod h1:sh3UhBhhKiHBX2Tjnrpq8qPvk28OxPz3hS0iMW6JdOY=
|
github.com/fluxcd/pkg/oci v0.63.0/go.mod h1:qMPz4njvm6hJzdyGSb8ydSqrapXxTQwJonxHIsdeXSQ=
|
||||||
github.com/fluxcd/pkg/runtime v0.96.0 h1:sF4ic8131BwbOE+T2pkiXlkr2gCaxAho500zlZJJLck=
|
github.com/fluxcd/pkg/runtime v0.103.0 h1:J5y5GPhWdkyqIUBlaI1FP2N02TtZmsjbWhhZubuTSFk=
|
||||||
github.com/fluxcd/pkg/runtime v0.96.0/go.mod h1:FyjNMFNAERkCsF/muTWJYU9MZOsq/m4Sc4aQk/EgQ9E=
|
github.com/fluxcd/pkg/runtime v0.103.0/go.mod h1:mbo2f3azo3yVQgm7XZGxQB6/2zvzQ5Wgtd8TjRRwwAw=
|
||||||
github.com/fluxcd/pkg/sourceignore v0.16.0 h1:28+IBmNM1rGNQysiAZXyilFMgS0kno/aJM4zSPgqu2A=
|
github.com/fluxcd/pkg/sourceignore v0.17.0 h1:Z72nruRMhC15zIEpWoDrAcJcJ1El6QDnP/aRDfE4WOA=
|
||||||
github.com/fluxcd/pkg/sourceignore v0.16.0/go.mod h1:Enjrk4gdk8t9VEp0dU3OHvMiS5ZHafZiL4H/FGNluh0=
|
github.com/fluxcd/pkg/sourceignore v0.17.0/go.mod h1:3e/VmYLId0pI/H5sK7W9Ibif+j0Ahns9RxNjDMtTTfY=
|
||||||
github.com/fluxcd/pkg/ssa v0.64.0 h1:B/8VYMIYMeRmolup2HOoWNqXh4UeXi6w2LvXXvl6MZM=
|
github.com/fluxcd/pkg/ssa v0.70.0 h1:IBylYPiTK1IEdCC2DvjKXIhwQcbd5VufXA9WS3zO+tE=
|
||||||
github.com/fluxcd/pkg/ssa v0.64.0/go.mod h1:RjvVjJIoRo1ecsv91yMuiqzO6cpNag80M6MOB/vrJdc=
|
github.com/fluxcd/pkg/ssa v0.70.0/go.mod h1:6igtlt7/zF+nNFQpa5ZAkkvtpL6o36NRU39/PqqC+Bg=
|
||||||
github.com/fluxcd/pkg/ssh v0.24.0 h1:hrPlxs0hhXf32DRqs68VbsXs0XfQMphyRVIk0rYYJa4=
|
github.com/fluxcd/pkg/ssh v0.24.0 h1:hrPlxs0hhXf32DRqs68VbsXs0XfQMphyRVIk0rYYJa4=
|
||||||
github.com/fluxcd/pkg/ssh v0.24.0/go.mod h1:xWammEqalrpurpcMiixJRXtynRQtBEoqheyU5F/vWrg=
|
github.com/fluxcd/pkg/ssh v0.24.0/go.mod h1:xWammEqalrpurpcMiixJRXtynRQtBEoqheyU5F/vWrg=
|
||||||
github.com/fluxcd/pkg/tar v0.17.0 h1:uNxbFXy8ly8C7fJ8D7w3rjTNJFrb4Hp1aY/30XkfvxY=
|
github.com/fluxcd/pkg/tar v0.17.0 h1:uNxbFXy8ly8C7fJ8D7w3rjTNJFrb4Hp1aY/30XkfvxY=
|
||||||
github.com/fluxcd/pkg/tar v0.17.0/go.mod h1:b1xyIRYDD0ket4SV5u0UXYv+ZdN/O/HmIO5jZQdHQls=
|
github.com/fluxcd/pkg/tar v0.17.0/go.mod h1:b1xyIRYDD0ket4SV5u0UXYv+ZdN/O/HmIO5jZQdHQls=
|
||||||
github.com/fluxcd/pkg/version v0.12.0 h1:MGbdbNf2D5wazMqAkNPn+Lh5j+oY0gxQJFTGyet5Hfc=
|
github.com/fluxcd/pkg/version v0.14.0 h1:T3llSc8sUnsuFrW5ng2ePSfXwGXUKv0YG9QXf0ErhWw=
|
||||||
github.com/fluxcd/pkg/version v0.12.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8=
|
github.com/fluxcd/pkg/version v0.14.0/go.mod h1:YHdg/78kzf+kCqS+SqSOiUxum5AjxlixiqwpX6AUZB8=
|
||||||
github.com/fluxcd/source-controller/api v1.7.4 h1:+EOVnRA9LmLxOx7J273l7IOEU39m+Slt/nQGBy69ygs=
|
github.com/fluxcd/source-controller/api v1.8.3 h1:WNEETjmp/YTZx5IMg9ewz2Wn8YzOVETeJJ0LIPivm40=
|
||||||
github.com/fluxcd/source-controller/api v1.7.4/go.mod h1:ruf49LEgZRBfcP+eshl2n9SX1MfHayCcViAIGnZcaDY=
|
github.com/fluxcd/source-controller/api v1.8.3/go.mod h1:sio4t49RDx+S1etHRFAEEw8qfVuw0KKlOg8bRVlEYPM=
|
||||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3 h1:SsVGAaMBxzvcgrOz/Kl6c2ybMHVqoiEFwtI+bDuSeSs=
|
github.com/fluxcd/source-watcher/api/v2 v2.1.1 h1:1LfT50ty+78MKKbschAZl28QbVqIyjaNq17KmW5wPJI=
|
||||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3/go.mod h1:Nx3QZweVyuhaOtSNrw+oxifG+qrakPvjgNAN9qlUTb0=
|
github.com/fluxcd/source-watcher/api/v2 v2.1.1/go.mod h1:6M1BzBGQRoIuSenSQlfJHwMVVobFPiNPxXqfN0IILc4=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
@@ -238,14 +242,14 @@ github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
|||||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
|
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
||||||
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
|
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
|
github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
|
||||||
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
|
||||||
@@ -297,21 +301,20 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
|
|||||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
|
github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
|
||||||
github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
|
github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
|
||||||
github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic=
|
github.com/google/go-github/v82 v82.0.0 h1:OH09ESON2QwKCUVMYmMcVu1IFKFoaZHwqYaUtr/MVfk=
|
||||||
github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI=
|
github.com/google/go-github/v82 v82.0.0/go.mod h1:hQ6Xo0VKfL8RZ7z1hSfB4fvISg0QqHOqe9BP0qo+WvM=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ=
|
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
|
||||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
@@ -326,12 +329,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
|||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
|
||||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
|
||||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
|
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
|
||||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -381,8 +382,8 @@ github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PW
|
|||||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -448,10 +449,10 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
|
|||||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
|
||||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
|
||||||
github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q=
|
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||||
github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
@@ -485,15 +486,15 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
|||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||||
github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ=
|
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
|
||||||
github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI=
|
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
|
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=
|
||||||
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
|
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
|
||||||
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
|
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
|
||||||
@@ -511,6 +512,7 @@ github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
|||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||||
@@ -538,8 +540,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U=
|
github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U=
|
||||||
github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
|
github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
|
||||||
github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=
|
|
||||||
github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg=
|
|
||||||
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
|
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
|
||||||
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||||
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=
|
github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo=
|
||||||
@@ -551,64 +551,66 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI
|
|||||||
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
|
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
|
||||||
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
gitlab.com/gitlab-org/api/client-go v0.142.5 h1:zvengEU958Fjwasi1V+9QNRw0viqNKkqUwvFD15XDZI=
|
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||||
gitlab.com/gitlab-org/api/client-go v0.142.5/go.mod h1:Ru5IRauphXt9qwmTzJD7ou1dH7Gc6pnsdFWEiMMpmB0=
|
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||||
|
gitlab.com/gitlab-org/api/client-go v1.29.0 h1:3KnF6vENry/9v9eVrnLi2OfBV0m/WSrwh3RcxgH/hkA=
|
||||||
|
gitlab.com/gitlab-org/api/client-go v1.29.0/go.mod h1:6i3EZtC6gKiTTmDwp+f6r/Yi9OY4AaYubl5B3yXEdHE=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0 h1:/Rij/t18Y7rUayNg7Id6rPrEnHgorxYabm2E6wUdPP4=
|
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk=
|
||||||
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0/go.mod h1:AdyDPn6pkbkt2w01n3BubRVk7xAsCRq1Yg1mpfyA/0E=
|
go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4=
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0 h1:NLnZybb9KkfMXPwZhd5diBYJoVxiO9Qa06dacEA7ySY=
|
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8=
|
||||||
go.opentelemetry.io/contrib/exporters/autoexport v0.63.0/go.mod h1:OvRg7gm5WRSCtxzGSsrFHbDLToYlStHNZQ+iPNIyD6g=
|
go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0 h1:OMqPldHt79PqWKOMYIAQs3CxAi7RLgPxwfFSwr4ZxtM=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.14.0/go.mod h1:1biG4qiqTxKiUCtoWDPpL3fB3KxVwCiGw81j3nKMuHE=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0 h1:QQqYw3lkrzwVsoEX0w//EhH/TCnpRdEenKBOOEIMjWc=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.14.0/go.mod h1:gSVQcr17jk2ig4jqJ2DX30IdWH251JcNAecvrqTxH1s=
|
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
|
go.opentelemetry.io/otel/exporters/prometheus v0.65.0 h1:jOveH/b4lU9HT7y+Gfamf18BqlOuz2PWEvs8yM7Q6XE=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk=
|
go.opentelemetry.io/otel/exporters/prometheus v0.65.0/go.mod h1:i1P8pcumauPtUI4YNopea1dhzEMuEqWP1xoUZDylLHo=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
|
||||||
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
|
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
|
||||||
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
|
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
|
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
|
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
@@ -622,15 +624,15 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -646,10 +648,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -659,8 +661,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -686,8 +688,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -697,8 +699,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -709,8 +711,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -719,24 +721,23 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||||
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/api v0.261.0 h1:3DoJ2GGibaCxNi1lhdScNMx9fTW87ujKHDgyHMMYdoA=
|
google.golang.org/api v0.261.0 h1:3DoJ2GGibaCxNi1lhdScNMx9fTW87ujKHDgyHMMYdoA=
|
||||||
google.golang.org/api v0.261.0/go.mod h1:nVH0ZK5C4tO0RdsMscleeTLY7I8m/Nt9IXxcXD2tfts=
|
google.golang.org/api v0.261.0/go.mod h1:nVH0ZK5C4tO0RdsMscleeTLY7I8m/Nt9IXxcXD2tfts=
|
||||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
@@ -760,39 +761,39 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||||
helm.sh/helm/v4 v4.1.0 h1:ytBbmQ7W2h1BLMyvkexnoG52JEDbYj9LTnnNgKRhiCI=
|
helm.sh/helm/v4 v4.1.4 h1:zwTrNkalG4f7SYigRSdQnYrTj0QEz1qzetzAlYoDVSo=
|
||||||
helm.sh/helm/v4 v4.1.0/go.mod h1:yH4qpYvTNBTHnkRSenhi1m7oEFKoN6iK3/rYyFJ00IQ=
|
helm.sh/helm/v4 v4.1.4/go.mod h1:5dSo8rRgn3OTkDAc/k0Ipw5/Q+BlqKIKZwa0XwSiINI=
|
||||||
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=
|
||||||
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=
|
||||||
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
|
k8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0=
|
||||||
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
|
k8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU=
|
||||||
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
|
||||||
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||||
k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE=
|
k8s.io/cli-runtime v0.35.2 h1:3DNctzpPNXavqyrm/FFiT60TLk4UjUxuUMYbKOE970E=
|
||||||
k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY=
|
k8s.io/cli-runtime v0.35.2/go.mod h1:G2Ieu0JidLm5m1z9b0OkFhnykvJ1w+vjbz1tR5OFKL0=
|
||||||
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
k8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=
|
||||||
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=
|
||||||
k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94=
|
k8s.io/component-base v0.35.2 h1:btgR+qNrpWuRSuvWSnQYsZy88yf5gVwemvz0yw79pGc=
|
||||||
k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0=
|
k8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0=
|
||||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||||
k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc=
|
k8s.io/kubectl v0.35.2 h1:aSmqhSOfsoG9NR5oR8OD5eMKpLN9x8oncxfqLHbJJII=
|
||||||
k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo=
|
k8s.io/kubectl v0.35.2/go.mod h1:+OJC779UsDJGxNPbHxCwvb4e4w9Eh62v/DNYU2TlsyM=
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
sigs.k8s.io/controller-runtime v0.23.0 h1:Ubi7klJWiwEWqDY+odSVZiFA0aDSevOCXpa38yCSYu8=
|
sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80=
|
||||||
sigs.k8s.io/controller-runtime v0.23.0/go.mod h1:DBOIr9NsprUqCZ1ZhsuJ0wAnQSIxY/C6VjZbmLgw0j0=
|
sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0=
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||||
sigs.k8s.io/kustomize/api v0.21.0 h1:I7nry5p8iDJbuRdYS7ez8MUvw7XVNPcIP5GkzzuXIIQ=
|
sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs=
|
||||||
sigs.k8s.io/kustomize/api v0.21.0/go.mod h1:XGVQuR5n2pXKWbzXHweZU683pALGw/AMVO4zU4iS8SE=
|
sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI=
|
||||||
sigs.k8s.io/kustomize/kyaml v0.21.0 h1:7mQAf3dUwf0wBerWJd8rXhVcnkk5Tvn/q91cGkaP6HQ=
|
sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI=
|
||||||
sigs.k8s.io/kustomize/kyaml v0.21.0/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
|
sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ=
|
||||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs=
|
||||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/theckman/yacspin"
|
"github.com/briandowns/spinner"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
@@ -45,6 +45,7 @@ import (
|
|||||||
|
|
||||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
||||||
"github.com/fluxcd/pkg/kustomize"
|
"github.com/fluxcd/pkg/kustomize"
|
||||||
|
buildfs "github.com/fluxcd/pkg/kustomize/filesys"
|
||||||
runclient "github.com/fluxcd/pkg/runtime/client"
|
runclient "github.com/fluxcd/pkg/runtime/client"
|
||||||
ssautil "github.com/fluxcd/pkg/ssa/utils"
|
ssautil "github.com/fluxcd/pkg/ssa/utils"
|
||||||
"sigs.k8s.io/kustomize/kyaml/filesys"
|
"sigs.k8s.io/kustomize/kyaml/filesys"
|
||||||
@@ -65,6 +66,65 @@ const (
|
|||||||
|
|
||||||
var defaultTimeout = 80 * time.Second
|
var defaultTimeout = 80 * time.Second
|
||||||
|
|
||||||
|
// fsBackend controls how the kustomization manifest is generated
|
||||||
|
// and which filesystem is used for the kustomize build.
|
||||||
|
type fsBackend interface {
|
||||||
|
Generate(gen *kustomize.Generator, dirPath string) (filesys.FileSystem, string, kustomize.Action, error)
|
||||||
|
Cleanup(dirPath string, action kustomize.Action) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// onDiskFsBackend writes to the source directory.
|
||||||
|
type onDiskFsBackend struct{}
|
||||||
|
|
||||||
|
func (onDiskFsBackend) Generate(gen *kustomize.Generator, dirPath string) (filesys.FileSystem, string, kustomize.Action, error) {
|
||||||
|
action, err := gen.WriteFile(dirPath, kustomize.WithSaveOriginalKustomization())
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", action, err
|
||||||
|
}
|
||||||
|
return filesys.MakeFsOnDisk(), dirPath, action, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (onDiskFsBackend) Cleanup(dirPath string, action kustomize.Action) error {
|
||||||
|
return kustomize.CleanDirectory(dirPath, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inMemoryFsBackend builds in an in-memory filesystem without modifying the source directory.
|
||||||
|
type inMemoryFsBackend struct{}
|
||||||
|
|
||||||
|
func (inMemoryFsBackend) Generate(gen *kustomize.Generator, dirPath string) (filesys.FileSystem, string, kustomize.Action, error) {
|
||||||
|
manifest, kfilePath, action, err := gen.GenerateManifest(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", action, err
|
||||||
|
}
|
||||||
|
|
||||||
|
absDirPath, err := filepath.Abs(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", action, fmt.Errorf("failed to resolve dirPath: %w", err)
|
||||||
|
}
|
||||||
|
absDirPath, err = filepath.EvalSymlinks(absDirPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", action, fmt.Errorf("failed to eval symlinks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", action, fmt.Errorf("failed to get working directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
diskFS, err := buildfs.MakeFsOnDiskSecure(cwd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", action, fmt.Errorf("failed to create secure filesystem: %w", err)
|
||||||
|
}
|
||||||
|
fs := buildfs.MakeFsInMemory(diskFS)
|
||||||
|
|
||||||
|
if err := fs.WriteFile(filepath.Join(absDirPath, filepath.Base(kfilePath)), manifest); err != nil {
|
||||||
|
return nil, "", action, err
|
||||||
|
}
|
||||||
|
return fs, absDirPath, action, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inMemoryFsBackend) Cleanup(string, kustomize.Action) error { return nil }
|
||||||
|
|
||||||
// Builder builds yaml manifests
|
// Builder builds yaml manifests
|
||||||
// It retrieves the kustomization object from the k8s cluster
|
// It retrieves the kustomization object from the k8s cluster
|
||||||
// and overlays the manifests with the resources specified in the resourcesPath
|
// and overlays the manifests with the resources specified in the resourcesPath
|
||||||
@@ -81,13 +141,15 @@ type Builder struct {
|
|||||||
action kustomize.Action
|
action kustomize.Action
|
||||||
kustomization *kustomizev1.Kustomization
|
kustomization *kustomizev1.Kustomization
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
spinner *yacspin.Spinner
|
spinner *spinner.Spinner
|
||||||
dryRun bool
|
dryRun bool
|
||||||
strictSubst bool
|
strictSubst bool
|
||||||
recursive bool
|
recursive bool
|
||||||
localSources map[string]string
|
localSources map[string]string
|
||||||
// diff needs to handle kustomizations one by one
|
// diff needs to handle kustomizations one by one, and opt-in to ignore kustomizations missing on cluster
|
||||||
singleKustomization bool
|
singleKustomization bool
|
||||||
|
ignoreNotFound bool
|
||||||
|
fsBackend fsBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuilderOptionFunc is a function that configures a Builder
|
// BuilderOptionFunc is a function that configures a Builder
|
||||||
@@ -111,22 +173,9 @@ func WithTimeout(timeout time.Duration) BuilderOptionFunc {
|
|||||||
|
|
||||||
func WithProgressBar() BuilderOptionFunc {
|
func WithProgressBar() BuilderOptionFunc {
|
||||||
return func(b *Builder) error {
|
return func(b *Builder) error {
|
||||||
// Add a spinner
|
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
|
||||||
cfg := yacspin.Config{
|
s.Suffix = " Kustomization diffing... " + spinnerDryRunMessage
|
||||||
Frequency: 100 * time.Millisecond,
|
b.spinner = s
|
||||||
CharSet: yacspin.CharSets[59],
|
|
||||||
Suffix: "Kustomization diffing...",
|
|
||||||
SuffixAutoColon: true,
|
|
||||||
Message: spinnerDryRunMessage,
|
|
||||||
StopCharacter: "✓",
|
|
||||||
StopColors: []string{"fgGreen"},
|
|
||||||
}
|
|
||||||
spinner, err := yacspin.New(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create spinner: %w", err)
|
|
||||||
}
|
|
||||||
b.spinner = spinner
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,6 +223,15 @@ func WithStrictSubstitute(strictSubstitute bool) BuilderOptionFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithIgnoreNotFound ignores NotFound errors from the cluster kustomization
|
||||||
|
// lookup as long as a local kustomization file is provided
|
||||||
|
func WithIgnoreNotFound(ignore bool) BuilderOptionFunc {
|
||||||
|
return func(b *Builder) error {
|
||||||
|
b.ignoreNotFound = ignore
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithIgnore sets ignore field
|
// WithIgnore sets ignore field
|
||||||
func WithIgnore(ignore []string) BuilderOptionFunc {
|
func WithIgnore(ignore []string) BuilderOptionFunc {
|
||||||
return func(b *Builder) error {
|
return func(b *Builder) error {
|
||||||
@@ -198,6 +256,16 @@ func WithLocalSources(localSources map[string]string) BuilderOptionFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithInMemoryBuild sets the in-memory build backend
|
||||||
|
func WithInMemoryBuild(inMemoryBuild bool) BuilderOptionFunc {
|
||||||
|
return func(b *Builder) error {
|
||||||
|
if inMemoryBuild {
|
||||||
|
b.fsBackend = inMemoryFsBackend{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithSingleKustomization sets the single kustomization field to true
|
// WithSingleKustomization sets the single kustomization field to true
|
||||||
func WithSingleKustomization() BuilderOptionFunc {
|
func WithSingleKustomization() BuilderOptionFunc {
|
||||||
return func(b *Builder) error {
|
return func(b *Builder) error {
|
||||||
@@ -215,7 +283,7 @@ func withClientConfigFrom(in *Builder) BuilderOptionFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// withClientConfigFrom copies spinner field
|
// withSpinnerFrom copies the spinner field from another Builder.
|
||||||
func withSpinnerFrom(in *Builder) BuilderOptionFunc {
|
func withSpinnerFrom(in *Builder) BuilderOptionFunc {
|
||||||
return func(b *Builder) error {
|
return func(b *Builder) error {
|
||||||
b.spinner = in.spinner
|
b.spinner = in.spinner
|
||||||
@@ -223,6 +291,14 @@ func withSpinnerFrom(in *Builder) BuilderOptionFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withFsBackend sets the build backend
|
||||||
|
func withFsBackend(s fsBackend) BuilderOptionFunc {
|
||||||
|
return func(b *Builder) error {
|
||||||
|
b.fsBackend = s
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// withKustomization sets the kustomization field
|
// withKustomization sets the kustomization field
|
||||||
func withKustomization(k *kustomizev1.Kustomization) BuilderOptionFunc {
|
func withKustomization(k *kustomizev1.Kustomization) BuilderOptionFunc {
|
||||||
return func(b *Builder) error {
|
return func(b *Builder) error {
|
||||||
@@ -258,10 +334,18 @@ func NewBuilder(name, resources string, opts ...BuilderOptionFunc) (*Builder, er
|
|||||||
b.timeout = defaultTimeout
|
b.timeout = defaultTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.fsBackend == nil {
|
||||||
|
b.fsBackend = onDiskFsBackend{}
|
||||||
|
}
|
||||||
|
|
||||||
if b.dryRun && b.kustomizationFile == "" && b.kustomization == nil {
|
if b.dryRun && b.kustomizationFile == "" && b.kustomization == nil {
|
||||||
return nil, fmt.Errorf("kustomization file is required for dry-run")
|
return nil, fmt.Errorf("kustomization file is required for dry-run")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.ignoreNotFound && b.kustomizationFile == "" {
|
||||||
|
return nil, fmt.Errorf("kustomization file is required when assuming new kustomizations")
|
||||||
|
}
|
||||||
|
|
||||||
if !b.dryRun && b.client == nil {
|
if !b.dryRun && b.client == nil {
|
||||||
return nil, fmt.Errorf("client is required for live run")
|
return nil, fmt.Errorf("client is required for live run")
|
||||||
}
|
}
|
||||||
@@ -360,10 +444,11 @@ func (b *Builder) build() (m resmap.ResMap, err error) {
|
|||||||
} else {
|
} else {
|
||||||
liveKus, err = b.getKustomization(ctx)
|
liveKus, err = b.getKustomization(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !apierrors.IsNotFound(err) || b.kustomization == nil {
|
unknownError := !apierrors.IsNotFound(err)
|
||||||
|
hasLocalFallback := b.kustomization != nil || b.ignoreNotFound
|
||||||
|
if unknownError || !hasLocalFallback {
|
||||||
return nil, fmt.Errorf("failed to get kustomization object: %w", err)
|
return nil, fmt.Errorf("failed to get kustomization object: %w", err)
|
||||||
}
|
}
|
||||||
// use provided Kustomization
|
|
||||||
liveKus = b.kustomization
|
liveKus = b.kustomization
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,9 +463,9 @@ func (b *Builder) build() (m resmap.ResMap, err error) {
|
|||||||
b.kustomization = k
|
b.kustomization = k
|
||||||
|
|
||||||
// generate kustomization.yaml if needed
|
// generate kustomization.yaml if needed
|
||||||
action, er := b.generate(*k, b.resourcesPath)
|
buildFS, buildDir, action, er := b.generate(*k, b.resourcesPath)
|
||||||
if er != nil {
|
if er != nil {
|
||||||
errf := kustomize.CleanDirectory(b.resourcesPath, action)
|
errf := b.fsBackend.Cleanup(b.resourcesPath, action)
|
||||||
err = fmt.Errorf("failed to generate kustomization.yaml: %w", fmt.Errorf("%v %v", er, errf))
|
err = fmt.Errorf("failed to generate kustomization.yaml: %w", fmt.Errorf("%v %v", er, errf))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -388,14 +473,14 @@ func (b *Builder) build() (m resmap.ResMap, err error) {
|
|||||||
b.action = action
|
b.action = action
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
errf := b.Cancel()
|
errf := b.fsBackend.Cleanup(b.resourcesPath, b.action)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = errf
|
err = errf
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// build the kustomization
|
// build the kustomization
|
||||||
m, err = b.do(ctx, *k, b.resourcesPath)
|
m, err = b.do(ctx, *k, buildFS, buildDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -436,6 +521,7 @@ func (b *Builder) kustomizationBuild(k *kustomizev1.Kustomization) ([]*unstructu
|
|||||||
WithRecursive(b.recursive),
|
WithRecursive(b.recursive),
|
||||||
WithLocalSources(b.localSources),
|
WithLocalSources(b.localSources),
|
||||||
WithDryRun(b.dryRun),
|
WithDryRun(b.dryRun),
|
||||||
|
withFsBackend(b.fsBackend),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -490,10 +576,10 @@ func (b *Builder) unMarshallKustomization() (*kustomizev1.Kustomization, error)
|
|||||||
return k, nil
|
return k, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (kustomize.Action, error) {
|
func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (filesys.FileSystem, string, kustomize.Action, error) {
|
||||||
data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization)
|
data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, "", kustomize.UnchangedAction, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// a scanner will be used down the line to parse the list
|
// a scanner will be used down the line to parse the list
|
||||||
@@ -505,12 +591,10 @@ func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath stri
|
|||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
return gen.WriteFile(dirPath, kustomize.WithSaveOriginalKustomization())
|
return b.fsBackend.Generate(gen, dirPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) {
|
func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, fs filesys.FileSystem, dirPath string) (resmap.ResMap, error) {
|
||||||
fs := filesys.MakeFsOnDisk()
|
|
||||||
|
|
||||||
// acquire the lock
|
// acquire the lock
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
@@ -734,24 +818,14 @@ func (b *Builder) Cancel() error {
|
|||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
err := kustomize.CleanDirectory(b.resourcesPath, b.action)
|
return b.fsBackend.Cleanup(b.resourcesPath, b.action)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) StartSpinner() error {
|
func (b *Builder) StartSpinner() error {
|
||||||
if b.spinner == nil {
|
if b.spinner == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
b.spinner.Start()
|
||||||
err := b.spinner.Start()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to start spinner: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,14 +833,6 @@ func (b *Builder) StopSpinner() error {
|
|||||||
if b.spinner == nil {
|
if b.spinner == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
b.spinner.Stop()
|
||||||
status := b.spinner.Status()
|
|
||||||
if status == yacspin.SpinnerRunning || status == yacspin.SpinnerPaused {
|
|
||||||
err := b.spinner.Stop()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to stop spinner: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,15 @@ package build
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
|
"github.com/fluxcd/pkg/kustomize"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
@@ -611,3 +614,229 @@ func Test_kustomizationPath(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chdirTemp changes to the given directory and restores the original on cleanup.
|
||||||
|
func chdirTemp(t *testing.T, dir string) {
|
||||||
|
t.Helper()
|
||||||
|
orig, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(dir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { os.Chdir(orig) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_inMemoryFsBackend_Generate(t *testing.T) {
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
chdirTemp(t, srcDir)
|
||||||
|
|
||||||
|
kusYAML := `apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- configmap.yaml
|
||||||
|
`
|
||||||
|
cmYAML := `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: test-cm
|
||||||
|
data:
|
||||||
|
key: value
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(srcDir, "kustomization.yaml"), []byte(kusYAML), 0o644); err != nil {
|
||||||
|
t.Fatalf("write: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(srcDir, "configmap.yaml"), []byte(cmYAML), 0o644); err != nil {
|
||||||
|
t.Fatalf("write: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// snapshot source dir
|
||||||
|
beforeFiles := map[string]string{}
|
||||||
|
filepath.Walk(srcDir, func(p string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, _ := os.ReadFile(p)
|
||||||
|
rel, _ := filepath.Rel(srcDir, p)
|
||||||
|
beforeFiles[rel] = string(data)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
ks := unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "kustomize.toolkit.fluxcd.io/v1",
|
||||||
|
"kind": "Kustomization",
|
||||||
|
"metadata": map[string]interface{}{"name": "test", "namespace": "default"},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"targetNamespace": "my-ns",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
gen := kustomize.NewGenerator(srcDir, ks)
|
||||||
|
|
||||||
|
backend := inMemoryFsBackend{}
|
||||||
|
fs, dir, action, err := backend.Generate(gen, srcDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if action != kustomize.UnchangedAction {
|
||||||
|
t.Errorf("expected UnchangedAction, got %q", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// kustomization.yaml should contain the merged targetNamespace
|
||||||
|
data, err := fs.ReadFile(filepath.Join(dir, "kustomization.yaml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile kustomization.yaml: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), "my-ns") {
|
||||||
|
t.Errorf("expected kustomization to contain targetNamespace, got:\n%s", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resource file should be readable from disk through the memory fs
|
||||||
|
data, err = fs.ReadFile(filepath.Join(dir, "configmap.yaml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile configmap.yaml: %v", err)
|
||||||
|
}
|
||||||
|
if diff := cmp.Diff(string(data), cmYAML); diff != "" {
|
||||||
|
t.Errorf("configmap mismatch: (-got +want)%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// source directory must be unmodified
|
||||||
|
afterFiles := map[string]string{}
|
||||||
|
filepath.Walk(srcDir, func(p string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data, _ := os.ReadFile(p)
|
||||||
|
rel, _ := filepath.Rel(srcDir, p)
|
||||||
|
afterFiles[rel] = string(data)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if diff := cmp.Diff(afterFiles, beforeFiles); diff != "" {
|
||||||
|
t.Errorf("source directory was modified: (-got +want)%s", diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_inMemoryFsBackend_Generate_parentRef(t *testing.T) {
|
||||||
|
// tmpDir/
|
||||||
|
// configmap.yaml (referenced as ../../configmap.yaml)
|
||||||
|
// overlay/sub/kustomization.yaml
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
chdirTemp(t, tmpDir)
|
||||||
|
|
||||||
|
cmYAML := `apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: parent-cm
|
||||||
|
data:
|
||||||
|
key: value
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "configmap.yaml"), []byte(cmYAML), 0o644); err != nil {
|
||||||
|
t.Fatalf("write: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
overlayDir := filepath.Join(tmpDir, "overlay", "sub")
|
||||||
|
if err := os.MkdirAll(overlayDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("mkdir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
kusYAML := `apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- ../../configmap.yaml
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(overlayDir, "kustomization.yaml"), []byte(kusYAML), 0o644); err != nil {
|
||||||
|
t.Fatalf("write: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ks := unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "kustomize.toolkit.fluxcd.io/v1",
|
||||||
|
"kind": "Kustomization",
|
||||||
|
"metadata": map[string]interface{}{"name": "test", "namespace": "default"},
|
||||||
|
"spec": map[string]interface{}{
|
||||||
|
"targetNamespace": "parent-ns",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
gen := kustomize.NewGenerator(overlayDir, ks)
|
||||||
|
|
||||||
|
backend := inMemoryFsBackend{}
|
||||||
|
fs, dir, _, err := backend.Generate(gen, overlayDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ../../configmap.yaml must resolve through the disk layer
|
||||||
|
m, err := kustomize.Build(fs, dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("kustomize.Build failed (parent ref not resolved): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resources := m.Resources()
|
||||||
|
if len(resources) != 1 {
|
||||||
|
t.Fatalf("expected 1 resource, got %d", len(resources))
|
||||||
|
}
|
||||||
|
if resources[0].GetName() != "parent-cm" {
|
||||||
|
t.Errorf("expected resource name parent-cm, got %s", resources[0].GetName())
|
||||||
|
}
|
||||||
|
if resources[0].GetNamespace() != "parent-ns" {
|
||||||
|
t.Errorf("expected namespace parent-ns, got %s", resources[0].GetNamespace())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_inMemoryFsBackend_Generate_outsideCwd(t *testing.T) {
|
||||||
|
// Two sibling temp dirs: one for the source tree, one as cwd.
|
||||||
|
// The kustomization references a file that exists on disk but is
|
||||||
|
// outside cwd, so the secure filesystem must reject it.
|
||||||
|
//
|
||||||
|
// parentDir/
|
||||||
|
// outside/configmap.yaml (exists but outside cwd)
|
||||||
|
// cwd/overlay/kustomization.yaml (references ../../outside/configmap.yaml)
|
||||||
|
parentDir := t.TempDir()
|
||||||
|
|
||||||
|
outsideDir := filepath.Join(parentDir, "outside")
|
||||||
|
if err := os.MkdirAll(outsideDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(outsideDir, "configmap.yaml"), []byte(`apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: outside-cm
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cwdDir := filepath.Join(parentDir, "cwd")
|
||||||
|
overlayDir := filepath.Join(cwdDir, "overlay")
|
||||||
|
if err := os.MkdirAll(overlayDir, 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(overlayDir, "kustomization.yaml"), []byte(`apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- ../../outside/configmap.yaml
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cwd to cwdDir so the secure root excludes outsideDir.
|
||||||
|
chdirTemp(t, cwdDir)
|
||||||
|
|
||||||
|
ks := unstructured.Unstructured{Object: map[string]interface{}{
|
||||||
|
"apiVersion": "kustomize.toolkit.fluxcd.io/v1",
|
||||||
|
"kind": "Kustomization",
|
||||||
|
"metadata": map[string]interface{}{"name": "test", "namespace": "default"},
|
||||||
|
}}
|
||||||
|
gen := kustomize.NewGenerator(overlayDir, ks)
|
||||||
|
|
||||||
|
backend := inMemoryFsBackend{}
|
||||||
|
fs, dir, _, err := backend.Generate(gen, overlayDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Generate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build must fail because the resource is outside the secure root.
|
||||||
|
_, err = kustomize.Build(fs, dir)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error when referencing resource outside cwd, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -173,14 +173,14 @@ func (b *Builder) diff() (string, bool, error) {
|
|||||||
|
|
||||||
// finished with Kustomization diff
|
// finished with Kustomization diff
|
||||||
if b.spinner != nil {
|
if b.spinner != nil {
|
||||||
b.spinner.Message(spinnerDryRunMessage)
|
b.spinner.Suffix = " " + spinnerDryRunMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.spinner != nil {
|
if b.spinner != nil {
|
||||||
b.spinner.Message("processing inventory")
|
b.spinner.Suffix = " processing inventory"
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.kustomization.Spec.Prune && len(diffErrs) == 0 {
|
if b.kustomization.Spec.Prune && len(diffErrs) == 0 {
|
||||||
@@ -204,7 +204,7 @@ func (b *Builder) diff() (string, bool, error) {
|
|||||||
|
|
||||||
func (b *Builder) kustomizationDiff(kustomization *kustomizev1.Kustomization) (string, bool, error) {
|
func (b *Builder) kustomizationDiff(kustomization *kustomizev1.Kustomization) (string, bool, error) {
|
||||||
if b.spinner != nil {
|
if b.spinner != nil {
|
||||||
b.spinner.Message(fmt.Sprintf("%s in %s", spinnerDryRunMessage, kustomization.Name))
|
b.spinner.Suffix = " " + fmt.Sprintf("%s in %s", spinnerDryRunMessage, kustomization.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceRef := kustomization.Spec.SourceRef.DeepCopy()
|
sourceRef := kustomization.Spec.SourceRef.DeepCopy()
|
||||||
@@ -230,6 +230,7 @@ func (b *Builder) kustomizationDiff(kustomization *kustomizev1.Kustomization) (s
|
|||||||
WithRecursive(b.recursive),
|
WithRecursive(b.recursive),
|
||||||
WithLocalSources(b.localSources),
|
WithLocalSources(b.localSources),
|
||||||
WithSingleKustomization(),
|
WithSingleKustomization(),
|
||||||
|
withFsBackend(b.fsBackend),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, err
|
return "", false, err
|
||||||
|
|||||||
68
internal/flags/receiver_type.go
Normal file
68
internal/flags/receiver_type.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 flags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var supportedReceiverTypes = []string{
|
||||||
|
notificationv1.GenericReceiver,
|
||||||
|
notificationv1.GenericHMACReceiver,
|
||||||
|
notificationv1.GitHubReceiver,
|
||||||
|
notificationv1.GitLabReceiver,
|
||||||
|
notificationv1.BitbucketReceiver,
|
||||||
|
notificationv1.HarborReceiver,
|
||||||
|
notificationv1.DockerHubReceiver,
|
||||||
|
notificationv1.QuayReceiver,
|
||||||
|
notificationv1.GCRReceiver,
|
||||||
|
notificationv1.NexusReceiver,
|
||||||
|
notificationv1.ACRReceiver,
|
||||||
|
notificationv1.CDEventsReceiver,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReceiverType string
|
||||||
|
|
||||||
|
func (r *ReceiverType) String() string {
|
||||||
|
return string(*r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReceiverType) Set(str string) error {
|
||||||
|
if strings.TrimSpace(str) == "" {
|
||||||
|
return fmt.Errorf("no receiver type given, please specify %s",
|
||||||
|
r.Description())
|
||||||
|
}
|
||||||
|
if !utils.ContainsItemString(supportedReceiverTypes, str) {
|
||||||
|
return fmt.Errorf("receiver type '%s' is not supported, must be one of: %s",
|
||||||
|
str, strings.Join(supportedReceiverTypes, ", "))
|
||||||
|
}
|
||||||
|
*r = ReceiverType(str)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReceiverType) Type() string {
|
||||||
|
return strings.Join(supportedReceiverTypes, "|")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReceiverType) Description() string {
|
||||||
|
return "the receiver type"
|
||||||
|
}
|
||||||
197
internal/plugin/catalog.go
Normal file
197
internal/plugin/catalog.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-retryablehttp"
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// defaultCatalogBase points at the latest GitHub release of fluxcd/plugins.
|
||||||
|
defaultCatalogBase = "https://github.com/fluxcd/plugins/releases/latest/download/"
|
||||||
|
envCatalogBase = "FLUXCD_PLUGIN_CATALOG"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CatalogClient fetches plugin manifests and catalogs from a remote URL.
|
||||||
|
type CatalogClient struct {
|
||||||
|
// BaseURL is the catalog base URL for fetching manifests.
|
||||||
|
BaseURL string
|
||||||
|
|
||||||
|
// HTTPClient is the HTTP client used for catalog requests.
|
||||||
|
HTTPClient *http.Client
|
||||||
|
|
||||||
|
// GetEnv returns the value of an environment variable.
|
||||||
|
GetEnv func(key string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCatalogClient returns a CatalogClient with production defaults.
|
||||||
|
func NewCatalogClient() *CatalogClient {
|
||||||
|
return &CatalogClient{
|
||||||
|
BaseURL: defaultCatalogBase,
|
||||||
|
HTTPClient: newHTTPClient(30 * time.Second),
|
||||||
|
GetEnv: func(key string) string { return "" },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// baseURL returns the effective catalog base URL.
|
||||||
|
func (c *CatalogClient) baseURL() string {
|
||||||
|
if env := c.GetEnv(envCatalogBase); env != "" {
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
return c.BaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchManifest fetches a single plugin manifest from the catalog.
|
||||||
|
func (c *CatalogClient) FetchManifest(name string) (*plugintypes.Manifest, error) {
|
||||||
|
url := c.baseURL() + name + ".yaml"
|
||||||
|
body, err := c.fetch(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("plugin %q not found in catalog", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest plugintypes.Manifest
|
||||||
|
if err := yaml.Unmarshal(body, &manifest); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse plugin manifest for %q: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if manifest.APIVersion != plugintypes.APIVersion {
|
||||||
|
return nil, fmt.Errorf("plugin %q has unsupported apiVersion %q (expected %q)", name, manifest.APIVersion, plugintypes.APIVersion)
|
||||||
|
}
|
||||||
|
if manifest.Kind != plugintypes.PluginKind {
|
||||||
|
return nil, fmt.Errorf("plugin %q has unexpected kind %q (expected %q)", name, manifest.Kind, plugintypes.PluginKind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &manifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchCatalog fetches the generated catalog.yaml.
|
||||||
|
func (c *CatalogClient) FetchCatalog() (*plugintypes.Catalog, error) {
|
||||||
|
url := c.baseURL() + "catalog.yaml"
|
||||||
|
body, err := c.fetch(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch plugin catalog: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var catalog plugintypes.Catalog
|
||||||
|
if err := yaml.Unmarshal(body, &catalog); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse plugin catalog: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if catalog.APIVersion != plugintypes.APIVersion {
|
||||||
|
return nil, fmt.Errorf("plugin catalog has unsupported apiVersion %q (expected %q)", catalog.APIVersion, plugintypes.APIVersion)
|
||||||
|
}
|
||||||
|
if catalog.Kind != plugintypes.CatalogKind {
|
||||||
|
return nil, fmt.Errorf("plugin catalog has unexpected kind %q (expected %q)", catalog.Kind, plugintypes.CatalogKind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &catalog, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxResponseBytes = 10 << 20 // 10 MiB
|
||||||
|
|
||||||
|
func (c *CatalogClient) fetch(url string) ([]byte, error) {
|
||||||
|
resp, err := c.HTTPClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP %d from %s", resp.StatusCode, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
return io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// newHTTPClient returns a retrying HTTP client with the given timeout.
|
||||||
|
func newHTTPClient(timeout time.Duration) *http.Client {
|
||||||
|
rc := retryablehttp.NewClient()
|
||||||
|
rc.RetryMax = 3
|
||||||
|
rc.Logger = nil
|
||||||
|
c := rc.StandardClient()
|
||||||
|
c.Timeout = timeout
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveVersion finds the requested version in the manifest.
|
||||||
|
// If version is empty, returns the first (latest) version.
|
||||||
|
func ResolveVersion(manifest *plugintypes.Manifest, version string) (*plugintypes.Version, error) {
|
||||||
|
if len(manifest.Versions) == 0 {
|
||||||
|
return nil, fmt.Errorf("plugin %q has no versions", manifest.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if version == "" {
|
||||||
|
return &manifest.Versions[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range manifest.Versions {
|
||||||
|
if manifest.Versions[i].Version == version {
|
||||||
|
return &manifest.Versions[i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("version %q not found for plugin %q", version, manifest.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolvePlatform finds the platform entry matching the given OS and arch.
|
||||||
|
func ResolvePlatform(pv *plugintypes.Version, goos, goarch string) (*plugintypes.Platform, error) {
|
||||||
|
for i := range pv.Platforms {
|
||||||
|
if pv.Platforms[i].OS == goos && pv.Platforms[i].Arch == goarch {
|
||||||
|
return &pv.Platforms[i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no binary for %s/%s", goos, goarch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DigestMatch holds the version and platform resolved from a digest lookup.
|
||||||
|
type DigestMatch struct {
|
||||||
|
Version *plugintypes.Version
|
||||||
|
Platform *plugintypes.Platform
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveByDigest scans all versions and platforms for a checksum matching
|
||||||
|
// digest. The digest must be in "algorithm:hex" format (e.g.
|
||||||
|
// "sha256:06e0a38..."). Only platforms matching goos/goarch are considered.
|
||||||
|
// Returns the first match (versions are ordered newest-first in the manifest).
|
||||||
|
func ResolveByDigest(manifest *plugintypes.Manifest, digest, goos, goarch string) (*DigestMatch, error) {
|
||||||
|
if len(manifest.Versions) == 0 {
|
||||||
|
return nil, fmt.Errorf("plugin %q has no versions", manifest.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range manifest.Versions {
|
||||||
|
for j := range manifest.Versions[i].Platforms {
|
||||||
|
p := &manifest.Versions[i].Platforms[j]
|
||||||
|
if p.OS == goos && p.Arch == goarch && p.Checksum == digest {
|
||||||
|
return &DigestMatch{
|
||||||
|
Version: &manifest.Versions[i],
|
||||||
|
Platform: p,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("digest %q not found for plugin %q on %s/%s", digest, manifest.Name, goos, goarch)
|
||||||
|
}
|
||||||
308
internal/plugin/catalog_test.go
Normal file
308
internal/plugin/catalog_test.go
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFetchManifest(t *testing.T) {
|
||||||
|
manifest := `
|
||||||
|
apiVersion: cli.fluxcd.io/v1beta1
|
||||||
|
kind: Plugin
|
||||||
|
name: operator
|
||||||
|
description: Flux Operator CLI
|
||||||
|
bin: flux-operator
|
||||||
|
versions:
|
||||||
|
- version: 0.45.0
|
||||||
|
platforms:
|
||||||
|
- os: linux
|
||||||
|
arch: amd64
|
||||||
|
url: https://example.com/flux-operator_0.45.0_linux_amd64.tar.gz
|
||||||
|
checksum: sha256:abc123
|
||||||
|
`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/operator.yaml" {
|
||||||
|
w.Write([]byte(manifest))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := &CatalogClient{
|
||||||
|
BaseURL: server.URL + "/",
|
||||||
|
HTTPClient: server.Client(),
|
||||||
|
GetEnv: func(key string) string { return "" },
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := client.FetchManifest("operator")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if m.Name != "operator" {
|
||||||
|
t.Errorf("expected name 'operator', got %q", m.Name)
|
||||||
|
}
|
||||||
|
if m.Bin != "flux-operator" {
|
||||||
|
t.Errorf("expected bin 'flux-operator', got %q", m.Bin)
|
||||||
|
}
|
||||||
|
if len(m.Versions) != 1 {
|
||||||
|
t.Fatalf("expected 1 version, got %d", len(m.Versions))
|
||||||
|
}
|
||||||
|
if m.Versions[0].Version != "0.45.0" {
|
||||||
|
t.Errorf("expected version '0.45.0', got %q", m.Versions[0].Version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchManifestNotFound(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := &CatalogClient{
|
||||||
|
BaseURL: server.URL + "/",
|
||||||
|
HTTPClient: server.Client(),
|
||||||
|
GetEnv: func(key string) string { return "" },
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := client.FetchManifest("nonexistent")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchCatalog(t *testing.T) {
|
||||||
|
catalog := `
|
||||||
|
apiVersion: cli.fluxcd.io/v1beta1
|
||||||
|
kind: PluginCatalog
|
||||||
|
plugins:
|
||||||
|
- name: operator
|
||||||
|
description: Flux Operator CLI
|
||||||
|
homepage: https://fluxoperator.dev/
|
||||||
|
source: https://github.com/controlplaneio-fluxcd/flux-operator
|
||||||
|
license: AGPL-3.0
|
||||||
|
- name: schema
|
||||||
|
description: CRD schemas
|
||||||
|
homepage: https://example.com/
|
||||||
|
source: https://github.com/example/flux-schema
|
||||||
|
license: Apache-2.0
|
||||||
|
`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/catalog.yaml" {
|
||||||
|
w.Write([]byte(catalog))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := &CatalogClient{
|
||||||
|
BaseURL: server.URL + "/",
|
||||||
|
HTTPClient: server.Client(),
|
||||||
|
GetEnv: func(key string) string { return "" },
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := client.FetchCatalog()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(c.Plugins) != 2 {
|
||||||
|
t.Fatalf("expected 2 plugins, got %d", len(c.Plugins))
|
||||||
|
}
|
||||||
|
if c.Plugins[0].Name != "operator" {
|
||||||
|
t.Errorf("expected name 'operator', got %q", c.Plugins[0].Name)
|
||||||
|
}
|
||||||
|
if c.Plugins[1].Name != "schema" {
|
||||||
|
t.Errorf("expected name 'schema', got %q", c.Plugins[1].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogEnvOverride(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/custom/catalog.yaml" {
|
||||||
|
w.Write([]byte(`apiVersion: cli.fluxcd.io/v1beta1
|
||||||
|
kind: PluginCatalog
|
||||||
|
plugins: []
|
||||||
|
`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := &CatalogClient{
|
||||||
|
BaseURL: "https://should-not-be-used/",
|
||||||
|
HTTPClient: server.Client(),
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == envCatalogBase {
|
||||||
|
return server.URL + "/custom/"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := client.FetchCatalog()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(c.Plugins) != 0 {
|
||||||
|
t.Fatalf("expected 0 plugins, got %d", len(c.Plugins))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveVersion(t *testing.T) {
|
||||||
|
manifest := &plugintypes.Manifest{
|
||||||
|
Name: "operator",
|
||||||
|
Versions: []plugintypes.Version{
|
||||||
|
{Version: "0.45.0"},
|
||||||
|
{Version: "0.44.0"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("latest", func(t *testing.T) {
|
||||||
|
v, err := ResolveVersion(manifest, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if v.Version != "0.45.0" {
|
||||||
|
t.Errorf("expected '0.45.0', got %q", v.Version)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("specific", func(t *testing.T) {
|
||||||
|
v, err := ResolveVersion(manifest, "0.44.0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if v.Version != "0.44.0" {
|
||||||
|
t.Errorf("expected '0.44.0', got %q", v.Version)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not found", func(t *testing.T) {
|
||||||
|
_, err := ResolveVersion(manifest, "0.99.0")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no versions", func(t *testing.T) {
|
||||||
|
_, err := ResolveVersion(&plugintypes.Manifest{Name: "empty"}, "")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolvePlatform(t *testing.T) {
|
||||||
|
pv := &plugintypes.Version{
|
||||||
|
Version: "0.45.0",
|
||||||
|
Platforms: []plugintypes.Platform{
|
||||||
|
{OS: "darwin", Arch: "arm64", URL: "https://example.com/darwin_arm64.tar.gz"},
|
||||||
|
{OS: "linux", Arch: "amd64", URL: "https://example.com/linux_amd64.tar.gz"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("found", func(t *testing.T) {
|
||||||
|
p, err := ResolvePlatform(pv, "darwin", "arm64")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if p.OS != "darwin" || p.Arch != "arm64" {
|
||||||
|
t.Errorf("unexpected platform: %s/%s", p.OS, p.Arch)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not found", func(t *testing.T) {
|
||||||
|
_, err := ResolvePlatform(pv, "windows", "amd64")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveByDigest(t *testing.T) {
|
||||||
|
manifest := &plugintypes.Manifest{
|
||||||
|
Name: "operator",
|
||||||
|
Versions: []plugintypes.Version{
|
||||||
|
{
|
||||||
|
Version: "0.46.0",
|
||||||
|
Platforms: []plugintypes.Platform{
|
||||||
|
{OS: "linux", Arch: "amd64", URL: "https://example.com/v46_linux.tar.gz", Checksum: "sha256:aaaa"},
|
||||||
|
{OS: "darwin", Arch: "arm64", URL: "https://example.com/v46_darwin.tar.gz", Checksum: "sha256:bbbb"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: "0.45.0",
|
||||||
|
Platforms: []plugintypes.Platform{
|
||||||
|
{OS: "linux", Arch: "amd64", URL: "https://example.com/v45_linux.tar.gz", Checksum: "sha256:cccc"},
|
||||||
|
{OS: "darwin", Arch: "arm64", URL: "https://example.com/v45_darwin.tar.gz", Checksum: "sha256:dddd"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("found in latest version", func(t *testing.T) {
|
||||||
|
dm, err := ResolveByDigest(manifest, "sha256:aaaa", "linux", "amd64")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if dm.Version.Version != "0.46.0" {
|
||||||
|
t.Errorf("expected version '0.46.0', got %q", dm.Version.Version)
|
||||||
|
}
|
||||||
|
if dm.Platform.Checksum != "sha256:aaaa" {
|
||||||
|
t.Errorf("expected checksum 'sha256:aaaa', got %q", dm.Platform.Checksum)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("found in older version", func(t *testing.T) {
|
||||||
|
dm, err := ResolveByDigest(manifest, "sha256:cccc", "linux", "amd64")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if dm.Version.Version != "0.45.0" {
|
||||||
|
t.Errorf("expected version '0.45.0', got %q", dm.Version.Version)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wrong platform", func(t *testing.T) {
|
||||||
|
// sha256:bbbb exists for darwin/arm64, not linux/amd64.
|
||||||
|
_, err := ResolveByDigest(manifest, "sha256:bbbb", "linux", "amd64")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not found", func(t *testing.T) {
|
||||||
|
_, err := ResolveByDigest(manifest, "sha256:nonexistent", "linux", "amd64")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no versions", func(t *testing.T) {
|
||||||
|
_, err := ResolveByDigest(&plugintypes.Manifest{Name: "empty"}, "sha256:abc", "linux", "amd64")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
75
internal/plugin/completion.go
Normal file
75
internal/plugin/completion.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// commandFunc is an alias to allow DI in tests.
|
||||||
|
var commandFunc = exec.Command
|
||||||
|
|
||||||
|
// CompleteFunc returns a ValidArgsFunction that delegates completion
|
||||||
|
// to the plugin binary via Cobra's __complete protocol.
|
||||||
|
func CompleteFunc(pluginPath string) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
completeArgs := append([]string{"__complete"}, args...)
|
||||||
|
completeArgs = append(completeArgs, toComplete)
|
||||||
|
|
||||||
|
out, err := commandFunc(pluginPath, completeArgs...).Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseCompletionOutput(string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCompletionOutput parses Cobra's __complete output format.
|
||||||
|
// Each line is a completion, last line is :<directive_int>.
|
||||||
|
func parseCompletionOutput(out string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
out = strings.TrimRight(out, "\n")
|
||||||
|
if out == "" {
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
lines := strings.Split(out, "\n")
|
||||||
|
|
||||||
|
// Last line is the directive in format ":N"
|
||||||
|
lastLine := lines[len(lines)-1]
|
||||||
|
completions := lines[:len(lines)-1]
|
||||||
|
|
||||||
|
directive := cobra.ShellCompDirectiveDefault
|
||||||
|
if strings.HasPrefix(lastLine, ":") {
|
||||||
|
if val, err := strconv.Atoi(lastLine[1:]); err == nil {
|
||||||
|
directive = cobra.ShellCompDirective(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []string
|
||||||
|
for _, c := range completions {
|
||||||
|
if c == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
results = append(results, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, directive
|
||||||
|
}
|
||||||
80
internal/plugin/completion_test.go
Normal file
80
internal/plugin/completion_test.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseCompletionOutput(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expectedCompletions []string
|
||||||
|
expectedDirective cobra.ShellCompDirective
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "standard output",
|
||||||
|
input: "instance\nrset\nrsip\nall\n:4\n",
|
||||||
|
expectedCompletions: []string{"instance", "rset", "rsip", "all"},
|
||||||
|
expectedDirective: cobra.ShellCompDirective(4),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default directive",
|
||||||
|
input: "foo\nbar\n:0\n",
|
||||||
|
expectedCompletions: []string{"foo", "bar"},
|
||||||
|
expectedDirective: cobra.ShellCompDirectiveDefault,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with descriptions",
|
||||||
|
input: "get\tGet resources\nbuild\tBuild resources\n:4\n",
|
||||||
|
expectedCompletions: []string{"get\tGet resources", "build\tBuild resources"},
|
||||||
|
expectedDirective: cobra.ShellCompDirective(4),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty completions",
|
||||||
|
input: ":4\n",
|
||||||
|
expectedCompletions: nil,
|
||||||
|
expectedDirective: cobra.ShellCompDirective(4),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty input",
|
||||||
|
input: "",
|
||||||
|
expectedCompletions: nil,
|
||||||
|
expectedDirective: cobra.ShellCompDirectiveError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
completions, directive := parseCompletionOutput(tt.input)
|
||||||
|
if directive != tt.expectedDirective {
|
||||||
|
t.Errorf("directive: got %d, want %d", directive, tt.expectedDirective)
|
||||||
|
}
|
||||||
|
if len(completions) != len(tt.expectedCompletions) {
|
||||||
|
t.Fatalf("completions count: got %d, want %d", len(completions), len(tt.expectedCompletions))
|
||||||
|
}
|
||||||
|
for i, c := range completions {
|
||||||
|
if c != tt.expectedCompletions[i] {
|
||||||
|
t.Errorf("completion[%d]: got %q, want %q", i, c, tt.expectedCompletions[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
205
internal/plugin/discovery.go
Normal file
205
internal/plugin/discovery.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pluginPrefix = "flux-"
|
||||||
|
defaultDirName = "plugins"
|
||||||
|
defaultBaseDir = ".fluxcd"
|
||||||
|
envPluginDir = "FLUXCD_PLUGINS"
|
||||||
|
)
|
||||||
|
|
||||||
|
// reservedNames are command names that cannot be used as plugin names.
|
||||||
|
var reservedNames = map[string]bool{
|
||||||
|
"plugin": true,
|
||||||
|
"help": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin represents a discovered plugin binary.
|
||||||
|
type Plugin struct {
|
||||||
|
// Name is the plugin name, e.g. "operator" (derived from "flux-operator").
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Path is the absolute path to the plugin binary.
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler discovers and executes plugins. Uses dependency injection
|
||||||
|
// for testability.
|
||||||
|
type Handler struct {
|
||||||
|
// ReadDir lists directory entries.
|
||||||
|
ReadDir func(name string) ([]os.DirEntry, error)
|
||||||
|
|
||||||
|
// Stat returns file info, following symlinks.
|
||||||
|
Stat func(name string) (os.FileInfo, error)
|
||||||
|
|
||||||
|
// GetEnv returns the value of an environment variable.
|
||||||
|
GetEnv func(key string) string
|
||||||
|
|
||||||
|
// HomeDir returns the current user's home directory.
|
||||||
|
HomeDir func() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler returns a Handler with production defaults.
|
||||||
|
func NewHandler() *Handler {
|
||||||
|
return &Handler{
|
||||||
|
ReadDir: os.ReadDir,
|
||||||
|
Stat: os.Stat,
|
||||||
|
GetEnv: os.Getenv,
|
||||||
|
HomeDir: os.UserHomeDir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover scans the plugin directory for executables matching flux-*.
|
||||||
|
// It skips builtins, reserved names, directories, non-executable files,
|
||||||
|
// and broken symlinks.
|
||||||
|
func (h *Handler) Discover(builtinNames []string) []Plugin {
|
||||||
|
dir := h.PluginDir()
|
||||||
|
if dir == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := h.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
builtins := make(map[string]bool, len(builtinNames))
|
||||||
|
for _, name := range builtinNames {
|
||||||
|
builtins[name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var plugins []Plugin
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
if !strings.HasPrefix(name, pluginPrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginName := pluginNameFromBinary(name)
|
||||||
|
if pluginName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if reservedNames[pluginName] || builtins[pluginName] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := filepath.Join(dir, name)
|
||||||
|
|
||||||
|
// Use Stat to follow symlinks and check the target.
|
||||||
|
info, err := h.Stat(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
// Broken symlink, permission denied, etc.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isExecutable(info) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins = append(plugins, Plugin{
|
||||||
|
Name: pluginName,
|
||||||
|
Path: fullPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginDir returns the plugin directory path. If FLUXCD_PLUGINS is set,
|
||||||
|
// returns that path. Otherwise returns ~/.fluxcd/plugins/.
|
||||||
|
// Does not create the directory — callers that write (install, update)
|
||||||
|
// should call EnsurePluginDir first.
|
||||||
|
func (h *Handler) PluginDir() string {
|
||||||
|
if dir := h.GetEnv(envPluginDir); dir != "" {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
home, err := h.HomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(home, defaultBaseDir, defaultDirName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsurePluginDir creates the plugin directory if it doesn't exist
|
||||||
|
// and returns the path. Best-effort — ignores mkdir errors for
|
||||||
|
// read-only filesystems. User-managed directories (via $FLUXCD_PLUGINS)
|
||||||
|
// are not auto-created.
|
||||||
|
func (h *Handler) EnsurePluginDir() string {
|
||||||
|
if envDir := h.GetEnv(envPluginDir); envDir != "" {
|
||||||
|
return envDir
|
||||||
|
}
|
||||||
|
|
||||||
|
home, err := h.HomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Join(home, defaultBaseDir, defaultDirName)
|
||||||
|
_ = os.MkdirAll(dir, 0o755)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// pluginNameFromBinary extracts the plugin name from a binary filename.
|
||||||
|
// "flux-operator" → "operator", "flux-my-tool" → "my-tool".
|
||||||
|
// Returns empty string for invalid names.
|
||||||
|
func pluginNameFromBinary(filename string) string {
|
||||||
|
if !strings.HasPrefix(filename, pluginPrefix) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.TrimPrefix(filename, pluginPrefix)
|
||||||
|
|
||||||
|
// On Windows, strip known extensions.
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
for _, ext := range []string{".exe", ".cmd", ".bat"} {
|
||||||
|
if strings.HasSuffix(strings.ToLower(name), ext) {
|
||||||
|
name = name[:len(name)-len(ext)]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// isExecutable checks if a file has the executable bit set.
|
||||||
|
// On Windows, this always returns true (executability is determined by extension).
|
||||||
|
func isExecutable(info os.FileInfo) bool {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return info.Mode().Perm()&0o111 != 0
|
||||||
|
}
|
||||||
302
internal/plugin/discovery_test.go
Normal file
302
internal/plugin/discovery_test.go
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockDirEntry implements os.DirEntry for testing.
|
||||||
|
type mockDirEntry struct {
|
||||||
|
name string
|
||||||
|
isDir bool
|
||||||
|
mode fs.FileMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockDirEntry) Name() string { return m.name }
|
||||||
|
func (m *mockDirEntry) IsDir() bool { return m.isDir }
|
||||||
|
func (m *mockDirEntry) Type() fs.FileMode { return m.mode }
|
||||||
|
func (m *mockDirEntry) Info() (fs.FileInfo, error) { return nil, nil }
|
||||||
|
|
||||||
|
// mockFileInfo implements os.FileInfo for testing.
|
||||||
|
type mockFileInfo struct {
|
||||||
|
name string
|
||||||
|
mode fs.FileMode
|
||||||
|
isDir bool
|
||||||
|
regular bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockFileInfo) Name() string { return m.name }
|
||||||
|
func (m *mockFileInfo) Size() int64 { return 0 }
|
||||||
|
func (m *mockFileInfo) Mode() fs.FileMode { return m.mode }
|
||||||
|
func (m *mockFileInfo) ModTime() time.Time { return time.Time{} }
|
||||||
|
func (m *mockFileInfo) IsDir() bool { return m.isDir }
|
||||||
|
func (m *mockFileInfo) Sys() any { return nil }
|
||||||
|
|
||||||
|
func newTestHandler(entries []os.DirEntry, statResults map[string]*mockFileInfo, envVars map[string]string) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
ReadDir: func(name string) ([]os.DirEntry, error) {
|
||||||
|
if entries == nil {
|
||||||
|
return nil, fmt.Errorf("directory not found")
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
},
|
||||||
|
Stat: func(name string) (os.FileInfo, error) {
|
||||||
|
if info, ok := statResults[name]; ok {
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("file not found: %s", name)
|
||||||
|
},
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
return envVars[key]
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) {
|
||||||
|
return "/home/testuser", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscover(t *testing.T) {
|
||||||
|
entries := []os.DirEntry{
|
||||||
|
&mockDirEntry{name: "flux-operator", mode: 0},
|
||||||
|
&mockDirEntry{name: "flux-local", mode: 0},
|
||||||
|
}
|
||||||
|
stats := map[string]*mockFileInfo{
|
||||||
|
"/test/plugins/flux-operator": {name: "flux-operator", mode: 0o755},
|
||||||
|
"/test/plugins/flux-local": {name: "flux-local", mode: 0o755},
|
||||||
|
}
|
||||||
|
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||||
|
|
||||||
|
plugins := h.Discover(nil)
|
||||||
|
if len(plugins) != 2 {
|
||||||
|
t.Fatalf("expected 2 plugins, got %d", len(plugins))
|
||||||
|
}
|
||||||
|
if plugins[0].Name != "operator" {
|
||||||
|
t.Errorf("expected name 'operator', got %q", plugins[0].Name)
|
||||||
|
}
|
||||||
|
if plugins[1].Name != "local" {
|
||||||
|
t.Errorf("expected name 'local', got %q", plugins[1].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverSkipsBuiltins(t *testing.T) {
|
||||||
|
entries := []os.DirEntry{
|
||||||
|
&mockDirEntry{name: "flux-version", mode: 0},
|
||||||
|
&mockDirEntry{name: "flux-get", mode: 0},
|
||||||
|
&mockDirEntry{name: "flux-operator", mode: 0},
|
||||||
|
}
|
||||||
|
stats := map[string]*mockFileInfo{
|
||||||
|
"/test/plugins/flux-version": {name: "flux-version", mode: 0o755},
|
||||||
|
"/test/plugins/flux-get": {name: "flux-get", mode: 0o755},
|
||||||
|
"/test/plugins/flux-operator": {name: "flux-operator", mode: 0o755},
|
||||||
|
}
|
||||||
|
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||||
|
|
||||||
|
plugins := h.Discover([]string{"version", "get"})
|
||||||
|
if len(plugins) != 1 {
|
||||||
|
t.Fatalf("expected 1 plugin, got %d", len(plugins))
|
||||||
|
}
|
||||||
|
if plugins[0].Name != "operator" {
|
||||||
|
t.Errorf("expected name 'operator', got %q", plugins[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverSkipsReserved(t *testing.T) {
|
||||||
|
entries := []os.DirEntry{
|
||||||
|
&mockDirEntry{name: "flux-plugin", mode: 0},
|
||||||
|
&mockDirEntry{name: "flux-help", mode: 0},
|
||||||
|
&mockDirEntry{name: "flux-operator", mode: 0},
|
||||||
|
}
|
||||||
|
stats := map[string]*mockFileInfo{
|
||||||
|
"/test/plugins/flux-plugin": {name: "flux-plugin", mode: 0o755},
|
||||||
|
"/test/plugins/flux-help": {name: "flux-help", mode: 0o755},
|
||||||
|
"/test/plugins/flux-operator": {name: "flux-operator", mode: 0o755},
|
||||||
|
}
|
||||||
|
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||||
|
|
||||||
|
plugins := h.Discover(nil)
|
||||||
|
if len(plugins) != 1 {
|
||||||
|
t.Fatalf("expected 1 plugin, got %d", len(plugins))
|
||||||
|
}
|
||||||
|
if plugins[0].Name != "operator" {
|
||||||
|
t.Errorf("expected name 'operator', got %q", plugins[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverSkipsNonExecutable(t *testing.T) {
|
||||||
|
entries := []os.DirEntry{
|
||||||
|
&mockDirEntry{name: "flux-noperm", mode: 0},
|
||||||
|
}
|
||||||
|
stats := map[string]*mockFileInfo{
|
||||||
|
"/test/plugins/flux-noperm": {name: "flux-noperm", mode: 0o644},
|
||||||
|
}
|
||||||
|
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||||
|
|
||||||
|
plugins := h.Discover(nil)
|
||||||
|
if len(plugins) != 0 {
|
||||||
|
t.Fatalf("expected 0 plugins, got %d", len(plugins))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverSkipsDirectories(t *testing.T) {
|
||||||
|
entries := []os.DirEntry{
|
||||||
|
&mockDirEntry{name: "flux-somedir", isDir: true, mode: fs.ModeDir},
|
||||||
|
}
|
||||||
|
stats := map[string]*mockFileInfo{}
|
||||||
|
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||||
|
|
||||||
|
plugins := h.Discover(nil)
|
||||||
|
if len(plugins) != 0 {
|
||||||
|
t.Fatalf("expected 0 plugins, got %d", len(plugins))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverFollowsSymlinks(t *testing.T) {
|
||||||
|
entries := []os.DirEntry{
|
||||||
|
// Symlink entry — Type() returns symlink, but Stat resolves to regular executable.
|
||||||
|
&mockDirEntry{name: "flux-linked", mode: fs.ModeSymlink},
|
||||||
|
}
|
||||||
|
stats := map[string]*mockFileInfo{
|
||||||
|
"/test/plugins/flux-linked": {name: "flux-linked", mode: 0o755},
|
||||||
|
}
|
||||||
|
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||||
|
|
||||||
|
plugins := h.Discover(nil)
|
||||||
|
if len(plugins) != 1 {
|
||||||
|
t.Fatalf("expected 1 plugin, got %d", len(plugins))
|
||||||
|
}
|
||||||
|
if plugins[0].Name != "linked" {
|
||||||
|
t.Errorf("expected name 'linked', got %q", plugins[0].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverDirNotExist(t *testing.T) {
|
||||||
|
h := newTestHandler(nil, nil, map[string]string{envPluginDir: "/nonexistent"})
|
||||||
|
|
||||||
|
plugins := h.Discover(nil)
|
||||||
|
if len(plugins) != 0 {
|
||||||
|
t.Fatalf("expected 0 plugins, got %d", len(plugins))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverCustomDir(t *testing.T) {
|
||||||
|
entries := []os.DirEntry{
|
||||||
|
&mockDirEntry{name: "flux-custom", mode: 0},
|
||||||
|
}
|
||||||
|
stats := map[string]*mockFileInfo{
|
||||||
|
"/custom/path/flux-custom": {name: "flux-custom", mode: 0o755},
|
||||||
|
}
|
||||||
|
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/custom/path"})
|
||||||
|
|
||||||
|
plugins := h.Discover(nil)
|
||||||
|
if len(plugins) != 1 {
|
||||||
|
t.Fatalf("expected 1 plugin, got %d", len(plugins))
|
||||||
|
}
|
||||||
|
if plugins[0].Path != "/custom/path/flux-custom" {
|
||||||
|
t.Errorf("expected path '/custom/path/flux-custom', got %q", plugins[0].Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverSkipsNonFluxPrefix(t *testing.T) {
|
||||||
|
entries := []os.DirEntry{
|
||||||
|
&mockDirEntry{name: "kubectl-foo", mode: 0},
|
||||||
|
&mockDirEntry{name: "random-binary", mode: 0},
|
||||||
|
&mockDirEntry{name: "flux-operator", mode: 0},
|
||||||
|
}
|
||||||
|
stats := map[string]*mockFileInfo{
|
||||||
|
"/test/plugins/flux-operator": {name: "flux-operator", mode: 0o755},
|
||||||
|
}
|
||||||
|
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||||
|
|
||||||
|
plugins := h.Discover(nil)
|
||||||
|
if len(plugins) != 1 {
|
||||||
|
t.Fatalf("expected 1 plugin, got %d", len(plugins))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiscoverBrokenSymlink(t *testing.T) {
|
||||||
|
entries := []os.DirEntry{
|
||||||
|
&mockDirEntry{name: "flux-broken", mode: fs.ModeSymlink},
|
||||||
|
}
|
||||||
|
// No stat entry for flux-broken — simulates a broken symlink.
|
||||||
|
stats := map[string]*mockFileInfo{}
|
||||||
|
h := newTestHandler(entries, stats, map[string]string{envPluginDir: "/test/plugins"})
|
||||||
|
|
||||||
|
plugins := h.Discover(nil)
|
||||||
|
if len(plugins) != 0 {
|
||||||
|
t.Fatalf("expected 0 plugins, got %d", len(plugins))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginNameFromBinary(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"flux-operator", "operator"},
|
||||||
|
{"flux-my-tool", "my-tool"},
|
||||||
|
{"flux-", ""},
|
||||||
|
{"notflux-thing", ""},
|
||||||
|
{"flux-a", "a"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
got := pluginNameFromBinary(tt.input)
|
||||||
|
if got != tt.expected {
|
||||||
|
t.Errorf("pluginNameFromBinary(%q) = %q, want %q", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginDir(t *testing.T) {
|
||||||
|
t.Run("uses env var", func(t *testing.T) {
|
||||||
|
h := &Handler{
|
||||||
|
GetEnv: func(key string) string {
|
||||||
|
if key == envPluginDir {
|
||||||
|
return "/custom/plugins"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
},
|
||||||
|
HomeDir: func() (string, error) {
|
||||||
|
return "/home/user", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dir := h.PluginDir()
|
||||||
|
if dir != "/custom/plugins" {
|
||||||
|
t.Errorf("expected '/custom/plugins', got %q", dir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("uses default", func(t *testing.T) {
|
||||||
|
h := &Handler{
|
||||||
|
GetEnv: func(key string) string { return "" },
|
||||||
|
HomeDir: func() (string, error) {
|
||||||
|
return "/home/user", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dir := h.PluginDir()
|
||||||
|
if dir != "/home/user/.fluxcd/plugins" {
|
||||||
|
t.Errorf("expected '/home/user/.fluxcd/plugins', got %q", dir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
30
internal/plugin/exec_unix.go
Normal file
30
internal/plugin/exec_unix.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exec replaces the current process with the plugin binary.
|
||||||
|
// This is what kubectl does — no signal forwarding or exit code propagation needed.
|
||||||
|
func Exec(path string, args []string) error {
|
||||||
|
return syscall.Exec(path, append([]string{path}, args...), os.Environ())
|
||||||
|
}
|
||||||
42
internal/plugin/exec_windows.go
Normal file
42
internal/plugin/exec_windows.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exec runs the plugin as a child process with full I/O passthrough.
|
||||||
|
// Matches kubectl's Windows fallback pattern.
|
||||||
|
func Exec(path string, args []string) error {
|
||||||
|
cmd := exec.Command(path, args...)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
err := cmd.Run()
|
||||||
|
if err == nil {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
os.Exit(exitErr.ExitCode())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
366
internal/plugin/install.go
Normal file
366
internal/plugin/install.go
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
|
plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Installer handles downloading, verifying, and installing plugins.
|
||||||
|
type Installer struct {
|
||||||
|
// HTTPClient is the HTTP client used for downloading plugin archives.
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInstaller returns an Installer with production defaults.
|
||||||
|
func NewInstaller() *Installer {
|
||||||
|
return &Installer{
|
||||||
|
HTTPClient: newHTTPClient(5 * time.Minute),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install downloads, verifies, extracts, and installs a plugin binary
|
||||||
|
// to the given plugin directory.
|
||||||
|
func (inst *Installer) Install(pluginDir string, manifest *plugintypes.Manifest, pv *plugintypes.Version, plat *plugintypes.Platform) error {
|
||||||
|
tmpFile, err := os.CreateTemp("", "flux-plugin-*")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
defer tmpFile.Close()
|
||||||
|
|
||||||
|
resp, err := inst.HTTPClient.Get(plat.URL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download plugin: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("failed to download plugin: HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher := sha256.New()
|
||||||
|
writer := io.MultiWriter(tmpFile, hasher)
|
||||||
|
if _, err := io.Copy(writer, resp.Body); err != nil {
|
||||||
|
return fmt.Errorf("failed to download plugin: %w", err)
|
||||||
|
}
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
actualChecksum := fmt.Sprintf("sha256:%x", hasher.Sum(nil))
|
||||||
|
if actualChecksum != plat.Checksum {
|
||||||
|
return fmt.Errorf("checksum verification failed (expected: %s, got: %s)", plat.Checksum, actualChecksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
// manifest.Bin is the single source of truth for the installed binary
|
||||||
|
// name (e.g. "flux-validate"). On Windows we always append ".exe".
|
||||||
|
// For archives it's also the entry name we look up; for raw binaries
|
||||||
|
// it's the rename target regardless of the URL's filename.
|
||||||
|
binName := manifest.Bin
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
binName += ".exe"
|
||||||
|
}
|
||||||
|
destPath := filepath.Join(pluginDir, binName)
|
||||||
|
|
||||||
|
// extractTarget is the path to match inside the archive. When the
|
||||||
|
// platform specifies an extractPath, use it verbatim (it may be a
|
||||||
|
// nested path like "bin/flux-operator"). Otherwise fall back to
|
||||||
|
// binName which matches by basename.
|
||||||
|
extractTarget := binName
|
||||||
|
if plat.ExtractPath != "" {
|
||||||
|
extractTarget = plat.ExtractPath
|
||||||
|
}
|
||||||
|
|
||||||
|
format, err := detectArchiveFormat(tmpFile.Name(), plat.URL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to detect plugin format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case formatZip:
|
||||||
|
err = extractFromZip(tmpFile.Name(), extractTarget, destPath)
|
||||||
|
case formatTarGz:
|
||||||
|
err = extractFromTarGz(tmpFile.Name(), extractTarget, destPath)
|
||||||
|
case formatTar:
|
||||||
|
err = extractFromTar(tmpFile.Name(), extractTarget, destPath)
|
||||||
|
case formatBinary:
|
||||||
|
err = copyPluginBinary(tmpFile.Name(), destPath)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected plugin format: %v", format)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
receipt := plugintypes.Receipt{
|
||||||
|
Name: manifest.Name,
|
||||||
|
Version: pv.Version,
|
||||||
|
InstalledAt: time.Now().UTC().Format(time.RFC3339),
|
||||||
|
Platform: *plat,
|
||||||
|
}
|
||||||
|
return writeReceipt(pluginDir, manifest.Name, &receipt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uninstall removes a plugin binary (or symlink) and its receipt from the
|
||||||
|
// plugin directory. Returns an error if the plugin is not installed.
|
||||||
|
func Uninstall(pluginDir, name string) error {
|
||||||
|
binName := pluginPrefix + name
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
binName += ".exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
binPath := filepath.Join(pluginDir, binName)
|
||||||
|
|
||||||
|
// Use Lstat so we detect symlinks without following them.
|
||||||
|
if _, err := os.Lstat(binPath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("plugin %q is not installed", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Remove(binPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove plugin binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receipt is optional (manually installed plugins don't have one).
|
||||||
|
if err := os.Remove(receiptPath(pluginDir, name)); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("failed to remove plugin receipt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadReceipt reads the install receipt for a plugin.
|
||||||
|
// Returns nil if no receipt exists.
|
||||||
|
func ReadReceipt(pluginDir, name string) *plugintypes.Receipt {
|
||||||
|
data, err := os.ReadFile(receiptPath(pluginDir, name))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var receipt plugintypes.Receipt
|
||||||
|
if err := yaml.Unmarshal(data, &receipt); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &receipt
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiptPath(pluginDir, name string) string {
|
||||||
|
return filepath.Join(pluginDir, pluginPrefix+name+".yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeReceipt(pluginDir, name string, receipt *plugintypes.Receipt) error {
|
||||||
|
data, err := yaml.Marshal(receipt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal receipt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(receiptPath(pluginDir, name), data, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// archiveFormat is the detected format of a downloaded plugin artifact.
|
||||||
|
type archiveFormat int
|
||||||
|
|
||||||
|
const (
|
||||||
|
formatBinary archiveFormat = iota
|
||||||
|
formatZip
|
||||||
|
formatTarGz
|
||||||
|
formatTar
|
||||||
|
)
|
||||||
|
|
||||||
|
// detectArchiveFormat determines the artifact format by first checking the URL
|
||||||
|
// extension, then falling back to magic-byte inspection. Returns formatBinary
|
||||||
|
// if neither indicates a known archive, in which case the downloaded file is
|
||||||
|
// installed as-is.
|
||||||
|
func detectArchiveFormat(path, url string) (archiveFormat, error) {
|
||||||
|
switch lower := strings.ToLower(url); {
|
||||||
|
case strings.HasSuffix(lower, ".zip"):
|
||||||
|
return formatZip, nil
|
||||||
|
case strings.HasSuffix(lower, ".tar.gz"), strings.HasSuffix(lower, ".tgz"):
|
||||||
|
return formatTarGz, nil
|
||||||
|
case strings.HasSuffix(lower, ".tar"):
|
||||||
|
return formatTar, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return formatBinary, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Read enough bytes to cover the tar "ustar" magic at offset 257.
|
||||||
|
var hdr [262]byte
|
||||||
|
n, err := io.ReadFull(f, hdr[:])
|
||||||
|
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
|
||||||
|
return formatBinary, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZIP: PK\x03\x04 (file), PK\x05\x06 (empty), PK\x07\x08 (spanned).
|
||||||
|
if n >= 4 && hdr[0] == 'P' && hdr[1] == 'K' &&
|
||||||
|
(hdr[2] == 0x03 || hdr[2] == 0x05 || hdr[2] == 0x07) {
|
||||||
|
return formatZip, nil
|
||||||
|
}
|
||||||
|
// gzip: \x1f\x8b
|
||||||
|
if n >= 2 && hdr[0] == 0x1f && hdr[1] == 0x8b {
|
||||||
|
return formatTarGz, nil
|
||||||
|
}
|
||||||
|
// tar: "ustar" at offset 257
|
||||||
|
if n >= 262 && string(hdr[257:262]) == "ustar" {
|
||||||
|
return formatTar, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatBinary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractFromTarGz extracts a named file from a tar.gz archive
|
||||||
|
// and streams it directly to destPath.
|
||||||
|
func extractFromTarGz(archivePath, targetName, destPath string) error {
|
||||||
|
f, err := os.Open(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
gr, err := gzip.NewReader(f)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read gzip: %w", err)
|
||||||
|
}
|
||||||
|
defer gr.Close()
|
||||||
|
|
||||||
|
return extractTarStream(gr, targetName, destPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractFromTar extracts a named file from an uncompressed tar archive
|
||||||
|
// and streams it directly to destPath.
|
||||||
|
func extractFromTar(archivePath, targetName, destPath string) error {
|
||||||
|
f, err := os.Open(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return extractTarStream(f, targetName, destPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchArchiveEntry reports whether the archive entry name matches the
|
||||||
|
// target. If target contains a path separator it is matched as an exact
|
||||||
|
// path; otherwise only the base name of the entry is compared.
|
||||||
|
func matchArchiveEntry(entryName, target string) bool {
|
||||||
|
if strings.Contains(target, "/") {
|
||||||
|
return entryName == target
|
||||||
|
}
|
||||||
|
return filepath.Base(entryName) == target
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractTarStream walks a tar stream and streams the first matching
|
||||||
|
// regular file to destPath. Non-regular entries (symlinks, devices,
|
||||||
|
// directories) and entries with unsafe paths are skipped.
|
||||||
|
func extractTarStream(r io.Reader, targetName, destPath string) error {
|
||||||
|
tr := tar.NewReader(r)
|
||||||
|
for {
|
||||||
|
header, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read tar: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !filepath.IsLocal(header.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !header.FileInfo().Mode().IsRegular() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matchArchiveEntry(header.Name, targetName) {
|
||||||
|
return writeStreamToFile(tr, destPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("binary %q not found in archive", targetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyPluginBinary copies a raw downloaded binary to destPath with 0755 mode.
|
||||||
|
// Used when the downloaded artifact is not an archive.
|
||||||
|
func copyPluginBinary(srcPath, destPath string) error {
|
||||||
|
src, err := os.Open(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open downloaded binary: %w", err)
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
return writeStreamToFile(src, destPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractFromZip extracts a named file from a zip archive and streams it
|
||||||
|
// directly to destPath. Non-regular entries (symlinks, devices, directories)
|
||||||
|
// and entries with unsafe paths are skipped.
|
||||||
|
func extractFromZip(archivePath, targetName, destPath string) error {
|
||||||
|
r, err := zip.OpenReader(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open zip: %w", err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
for _, f := range r.File {
|
||||||
|
if !filepath.IsLocal(f.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !f.FileInfo().Mode().IsRegular() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if matchArchiveEntry(f.Name, targetName) {
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open %q in zip: %w", targetName, err)
|
||||||
|
}
|
||||||
|
err = writeStreamToFile(rc, destPath)
|
||||||
|
rc.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("binary %q not found in archive", targetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeStreamToFile(r io.Reader, destPath string) error {
|
||||||
|
out, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create %s: %w", destPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(out, r); err != nil {
|
||||||
|
if closeErr := out.Close(); closeErr != nil {
|
||||||
|
return fmt.Errorf("failed to write plugin binary: %w (also failed to close file: %v)", err, closeErr)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to write plugin binary: %w", err)
|
||||||
|
}
|
||||||
|
return out.Close()
|
||||||
|
}
|
||||||
815
internal/plugin/install_test.go
Normal file
815
internal/plugin/install_test.go
Normal file
@@ -0,0 +1,815 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createTestTarGz creates a tar.gz archive containing a single file.
|
||||||
|
func createTestTarGz(name string, content []byte) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
gw := gzip.NewWriter(&buf)
|
||||||
|
tw := tar.NewWriter(gw)
|
||||||
|
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: name,
|
||||||
|
Mode: 0o755,
|
||||||
|
Size: int64(len(content)),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := tw.Write(content); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tw.Close()
|
||||||
|
gw.Close()
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestTar creates an uncompressed tar archive containing a single file.
|
||||||
|
func createTestTar(name string, content []byte) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
tw := tar.NewWriter(&buf)
|
||||||
|
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: name,
|
||||||
|
Mode: 0o755,
|
||||||
|
Size: int64(len(content)),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := tw.Write(content); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tw.Close()
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tarEntry describes a single entry for createTestTarGzMulti.
|
||||||
|
type tarEntry struct {
|
||||||
|
header tar.Header
|
||||||
|
content []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestTarGzMulti creates a tar.gz archive with arbitrary entries.
|
||||||
|
// Used to test rejection of unsafe or non-regular entries.
|
||||||
|
func createTestTarGzMulti(entries []tarEntry) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
gw := gzip.NewWriter(&buf)
|
||||||
|
tw := tar.NewWriter(gw)
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
hdr := e.header
|
||||||
|
hdr.Size = int64(len(e.content))
|
||||||
|
if err := tw.WriteHeader(&hdr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(e.content) > 0 {
|
||||||
|
if _, err := tw.Write(e.content); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tw.Close()
|
||||||
|
gw.Close()
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// zipEntry describes a single entry for createTestZip.
|
||||||
|
type zipEntry struct {
|
||||||
|
name string
|
||||||
|
mode fs.FileMode
|
||||||
|
content []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestZip creates a zip archive with arbitrary entries. Entries may
|
||||||
|
// carry Unix mode bits (e.g. os.ModeSymlink) to exercise non-regular files.
|
||||||
|
func createTestZip(entries []zipEntry) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zw := zip.NewWriter(&buf)
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
hdr := &zip.FileHeader{
|
||||||
|
Name: e.name,
|
||||||
|
Method: zip.Deflate,
|
||||||
|
}
|
||||||
|
mode := e.mode
|
||||||
|
if mode == 0 {
|
||||||
|
mode = 0o755
|
||||||
|
}
|
||||||
|
hdr.SetMode(mode)
|
||||||
|
|
||||||
|
w, err := zw.CreateHeader(hdr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := w.Write(e.content); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := zw.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstall(t *testing.T) {
|
||||||
|
binaryContent := []byte("#!/bin/sh\necho hello")
|
||||||
|
archive, err := createTestTarGz("flux-operator", binaryContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create test archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum := fmt.Sprintf("sha256:%x", sha256.Sum256(archive))
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(archive)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
manifest := &plugintypes.Manifest{
|
||||||
|
Name: "operator",
|
||||||
|
Bin: "flux-operator",
|
||||||
|
}
|
||||||
|
pv := &plugintypes.Version{Version: "0.45.0"}
|
||||||
|
plat := &plugintypes.Platform{
|
||||||
|
OS: "linux",
|
||||||
|
Arch: "amd64",
|
||||||
|
URL: server.URL + "/flux-operator_0.45.0_linux_amd64.tar.gz",
|
||||||
|
Checksum: checksum,
|
||||||
|
}
|
||||||
|
|
||||||
|
installer := &Installer{HTTPClient: server.Client()}
|
||||||
|
if err := installer.Install(pluginDir, manifest, pv, plat); err != nil {
|
||||||
|
t.Fatalf("install failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify binary was written.
|
||||||
|
binPath := filepath.Join(pluginDir, "flux-operator")
|
||||||
|
data, err := os.ReadFile(binPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("binary not found: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != string(binaryContent) {
|
||||||
|
t.Errorf("binary content mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify receipt was written.
|
||||||
|
receipt := ReadReceipt(pluginDir, "operator")
|
||||||
|
if receipt == nil {
|
||||||
|
t.Fatal("receipt not found")
|
||||||
|
}
|
||||||
|
if receipt.Version != "0.45.0" {
|
||||||
|
t.Errorf("expected version '0.45.0', got %q", receipt.Version)
|
||||||
|
}
|
||||||
|
if receipt.Name != "operator" {
|
||||||
|
t.Errorf("expected name 'operator', got %q", receipt.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstallChecksumMismatch(t *testing.T) {
|
||||||
|
binaryContent := []byte("#!/bin/sh\necho hello")
|
||||||
|
archive, err := createTestTarGz("flux-operator", binaryContent)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create test archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(archive)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
manifest := &plugintypes.Manifest{Name: "operator", Bin: "flux-operator"}
|
||||||
|
pv := &plugintypes.Version{Version: "0.45.0"}
|
||||||
|
plat := &plugintypes.Platform{
|
||||||
|
OS: "linux",
|
||||||
|
Arch: "amd64",
|
||||||
|
URL: server.URL + "/archive.tar.gz",
|
||||||
|
Checksum: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
}
|
||||||
|
|
||||||
|
installer := &Installer{HTTPClient: server.Client()}
|
||||||
|
err = installer.Install(pluginDir, manifest, pv, plat)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected checksum error, got nil")
|
||||||
|
}
|
||||||
|
if !bytes.Contains([]byte(err.Error()), []byte("checksum verification failed")) {
|
||||||
|
t.Errorf("expected checksum error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstallBinaryNotInArchive(t *testing.T) {
|
||||||
|
// Archive contains "wrong-name" instead of "flux-operator".
|
||||||
|
archive, err := createTestTarGz("wrong-name", []byte("content"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create test archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum := fmt.Sprintf("sha256:%x", sha256.Sum256(archive))
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(archive)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
manifest := &plugintypes.Manifest{Name: "operator", Bin: "flux-operator"}
|
||||||
|
pv := &plugintypes.Version{Version: "0.45.0"}
|
||||||
|
plat := &plugintypes.Platform{
|
||||||
|
OS: "linux",
|
||||||
|
Arch: "amd64",
|
||||||
|
URL: server.URL + "/archive.tar.gz",
|
||||||
|
Checksum: checksum,
|
||||||
|
}
|
||||||
|
|
||||||
|
installer := &Installer{HTTPClient: server.Client()}
|
||||||
|
err = installer.Install(pluginDir, manifest, pv, plat)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for missing binary, got nil")
|
||||||
|
}
|
||||||
|
if !bytes.Contains([]byte(err.Error()), []byte("not found in archive")) {
|
||||||
|
t.Errorf("expected 'not found in archive' error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUninstall(t *testing.T) {
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create fake binary and receipt.
|
||||||
|
binPath := filepath.Join(pluginDir, "flux-testplugin")
|
||||||
|
os.WriteFile(binPath, []byte("binary"), 0o755)
|
||||||
|
receiptPath := filepath.Join(pluginDir, "flux-testplugin.yaml")
|
||||||
|
os.WriteFile(receiptPath, []byte("name: testplugin"), 0o644)
|
||||||
|
|
||||||
|
if err := Uninstall(pluginDir, "testplugin"); err != nil {
|
||||||
|
t.Fatalf("uninstall failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(binPath); !os.IsNotExist(err) {
|
||||||
|
t.Error("binary was not removed")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(receiptPath); !os.IsNotExist(err) {
|
||||||
|
t.Error("receipt was not removed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUninstallNonExistent(t *testing.T) {
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
err := Uninstall(pluginDir, "nonexistent")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for non-existent plugin, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "is not installed") {
|
||||||
|
t.Errorf("expected 'is not installed' error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUninstallSymlink(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("symlinks require elevated privileges on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a real binary and symlink it into the plugin dir.
|
||||||
|
realBin := filepath.Join(t.TempDir(), "flux-operator")
|
||||||
|
os.WriteFile(realBin, []byte("real binary"), 0o755)
|
||||||
|
|
||||||
|
linkPath := filepath.Join(pluginDir, "flux-linked")
|
||||||
|
os.Symlink(realBin, linkPath)
|
||||||
|
|
||||||
|
if err := Uninstall(pluginDir, "linked"); err != nil {
|
||||||
|
t.Fatalf("uninstall symlink failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symlink should be removed.
|
||||||
|
if _, err := os.Lstat(linkPath); !os.IsNotExist(err) {
|
||||||
|
t.Error("symlink was not removed")
|
||||||
|
}
|
||||||
|
// Original binary should still exist.
|
||||||
|
if _, err := os.Stat(realBin); err != nil {
|
||||||
|
t.Error("original binary was removed — symlink removal should not affect target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUninstallManualBinary(t *testing.T) {
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
// Manually copied binary with no receipt.
|
||||||
|
binPath := filepath.Join(pluginDir, "flux-manual")
|
||||||
|
os.WriteFile(binPath, []byte("binary"), 0o755)
|
||||||
|
|
||||||
|
if err := Uninstall(pluginDir, "manual"); err != nil {
|
||||||
|
t.Fatalf("uninstall manual binary failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(binPath); !os.IsNotExist(err) {
|
||||||
|
t.Error("binary was not removed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadReceipt(t *testing.T) {
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
t.Run("exists", func(t *testing.T) {
|
||||||
|
receiptData := `name: operator
|
||||||
|
version: "0.45.0"
|
||||||
|
installedAt: "2026-03-28T20:05:00Z"
|
||||||
|
platform:
|
||||||
|
os: darwin
|
||||||
|
arch: arm64
|
||||||
|
url: https://example.com/archive.tar.gz
|
||||||
|
checksum: sha256:abc123
|
||||||
|
`
|
||||||
|
os.WriteFile(filepath.Join(pluginDir, "flux-operator.yaml"), []byte(receiptData), 0o644)
|
||||||
|
|
||||||
|
receipt := ReadReceipt(pluginDir, "operator")
|
||||||
|
if receipt == nil {
|
||||||
|
t.Fatal("expected receipt, got nil")
|
||||||
|
}
|
||||||
|
if receipt.Version != "0.45.0" {
|
||||||
|
t.Errorf("expected version '0.45.0', got %q", receipt.Version)
|
||||||
|
}
|
||||||
|
if receipt.Platform.OS != "darwin" {
|
||||||
|
t.Errorf("expected OS 'darwin', got %q", receipt.Platform.OS)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not exists", func(t *testing.T) {
|
||||||
|
receipt := ReadReceipt(pluginDir, "nonexistent")
|
||||||
|
if receipt != nil {
|
||||||
|
t.Error("expected nil receipt")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstallRawBinary(t *testing.T) {
|
||||||
|
// Bytes that don't match zip/gzip/tar magic — treated as a raw binary.
|
||||||
|
binaryContent := []byte("#!/bin/sh\necho raw plugin")
|
||||||
|
checksum := fmt.Sprintf("sha256:%x", sha256.Sum256(binaryContent))
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(binaryContent)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
manifest := &plugintypes.Manifest{
|
||||||
|
Name: "validate",
|
||||||
|
Bin: "flux-validate",
|
||||||
|
}
|
||||||
|
pv := &plugintypes.Version{Version: "1.2.3"}
|
||||||
|
plat := &plugintypes.Platform{
|
||||||
|
OS: runtime.GOOS,
|
||||||
|
Arch: runtime.GOARCH,
|
||||||
|
// URL filename deliberately differs from manifest.Bin — mimics a
|
||||||
|
// typical GitHub release asset that includes platform/version in
|
||||||
|
// the name. The installer must rename to manifest.Bin.
|
||||||
|
URL: server.URL + "/download/flux-validate-" + runtime.GOARCH + "-v1.2.3",
|
||||||
|
Checksum: checksum,
|
||||||
|
}
|
||||||
|
|
||||||
|
installer := &Installer{HTTPClient: server.Client()}
|
||||||
|
if err := installer.Install(pluginDir, manifest, pv, plat); err != nil {
|
||||||
|
t.Fatalf("install failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The installed file must be named exactly manifest.Bin (+ .exe on Windows),
|
||||||
|
// regardless of what the URL path looked like.
|
||||||
|
wantName := "flux-validate"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
wantName += ".exe"
|
||||||
|
}
|
||||||
|
binPath := filepath.Join(pluginDir, wantName)
|
||||||
|
data, err := os.ReadFile(binPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("binary not found at %s: %v", binPath, err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data, binaryContent) {
|
||||||
|
t.Errorf("binary content mismatch: got %q, want %q", data, binaryContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing should have been written under the URL-derived name.
|
||||||
|
urlDerived := filepath.Join(pluginDir, "flux-validate-"+runtime.GOARCH+"-v1.2.3")
|
||||||
|
if _, err := os.Stat(urlDerived); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("unexpected file at URL-derived path %s", urlDerived)
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
info, err := os.Stat(binPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("stat: %v", err)
|
||||||
|
}
|
||||||
|
if info.Mode()&0o111 == 0 {
|
||||||
|
t.Errorf("binary is not executable: mode %v", info.Mode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw binary install must still produce a receipt.
|
||||||
|
if receipt := ReadReceipt(pluginDir, "validate"); receipt == nil {
|
||||||
|
t.Fatal("receipt not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectArchiveFormat(t *testing.T) {
|
||||||
|
tarGz, err := createTestTarGz("bin", []byte("content"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("createTestTarGz: %v", err)
|
||||||
|
}
|
||||||
|
plainTar, err := createTestTar("bin", []byte("content"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("createTestTar: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
url string
|
||||||
|
content []byte
|
||||||
|
want archiveFormat
|
||||||
|
}{
|
||||||
|
// Extension-based detection takes precedence over content.
|
||||||
|
{"zip extension", "https://example.com/plugin.zip", []byte("ignored"), formatZip},
|
||||||
|
{"tar.gz extension", "https://example.com/plugin.tar.gz", []byte("ignored"), formatTarGz},
|
||||||
|
{"tgz extension", "https://example.com/plugin.tgz", []byte("ignored"), formatTarGz},
|
||||||
|
{"tar extension", "https://example.com/plugin.tar", []byte("ignored"), formatTar},
|
||||||
|
{"uppercase extension", "https://example.com/PLUGIN.ZIP", []byte("ignored"), formatZip},
|
||||||
|
|
||||||
|
// Magic-byte detection when extension is absent or unrecognized.
|
||||||
|
{"zip magic no extension", "https://example.com/download", []byte{'P', 'K', 0x03, 0x04, 0, 0, 0, 0}, formatZip},
|
||||||
|
{"gzip magic no extension", "https://example.com/download", tarGz, formatTarGz},
|
||||||
|
{"tar magic no extension", "https://example.com/download", plainTar, formatTar},
|
||||||
|
|
||||||
|
// Fallback to raw binary.
|
||||||
|
{"unknown content", "https://example.com/download", []byte("#!/bin/sh\necho hi"), formatBinary},
|
||||||
|
{"short file", "https://example.com/download", []byte("ab"), formatBinary},
|
||||||
|
{"empty file", "https://example.com/download", []byte{}, formatBinary},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
tmp := filepath.Join(t.TempDir(), "artifact")
|
||||||
|
if err := os.WriteFile(tmp, tc.content, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := detectArchiveFormat(tmp, tc.url)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("detectArchiveFormat: %v", err)
|
||||||
|
}
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("got %v, want %v", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractFromTarGz(t *testing.T) {
|
||||||
|
content := []byte("test binary content")
|
||||||
|
archive, err := createTestTarGz("flux-operator", content)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile := filepath.Join(t.TempDir(), "test.tar.gz")
|
||||||
|
os.WriteFile(tmpFile, archive, 0o644)
|
||||||
|
|
||||||
|
destPath := filepath.Join(t.TempDir(), "flux-operator")
|
||||||
|
if err := extractFromTarGz(tmpFile, "flux-operator", destPath); err != nil {
|
||||||
|
t.Fatalf("extract failed: %v", err)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(destPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read extracted file: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != string(content) {
|
||||||
|
t.Errorf("content mismatch: got %q, want %q", string(data), string(content))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractFromTarGzRejectsUnsafeEntries(t *testing.T) {
|
||||||
|
content := []byte("legit content")
|
||||||
|
|
||||||
|
// Archive contains, in order:
|
||||||
|
// 1. A symlink whose basename matches the target (must be skipped).
|
||||||
|
// 2. A regular entry with ".." in the path (must be skipped).
|
||||||
|
// 3. An absolute-path entry (must be skipped).
|
||||||
|
// 4. A legitimate regular file that must be extracted.
|
||||||
|
entries := []tarEntry{
|
||||||
|
{
|
||||||
|
header: tar.Header{
|
||||||
|
Name: "flux-operator",
|
||||||
|
Typeflag: tar.TypeSymlink,
|
||||||
|
Linkname: "/etc/passwd",
|
||||||
|
Mode: 0o777,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: tar.Header{
|
||||||
|
Name: "../flux-operator",
|
||||||
|
Typeflag: tar.TypeReg,
|
||||||
|
Mode: 0o755,
|
||||||
|
},
|
||||||
|
content: []byte("malicious"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: tar.Header{
|
||||||
|
Name: "/absolute/flux-operator",
|
||||||
|
Typeflag: tar.TypeReg,
|
||||||
|
Mode: 0o755,
|
||||||
|
},
|
||||||
|
content: []byte("malicious"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: tar.Header{
|
||||||
|
Name: "bin/flux-operator",
|
||||||
|
Typeflag: tar.TypeReg,
|
||||||
|
Mode: 0o755,
|
||||||
|
},
|
||||||
|
content: content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
archive, err := createTestTarGzMulti(entries)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("createTestTarGzMulti: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile := filepath.Join(t.TempDir(), "test.tar.gz")
|
||||||
|
os.WriteFile(tmpFile, archive, 0o644)
|
||||||
|
|
||||||
|
destPath := filepath.Join(t.TempDir(), "flux-operator")
|
||||||
|
if err := extractFromTarGz(tmpFile, "flux-operator", destPath); err != nil {
|
||||||
|
t.Fatalf("extract failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(destPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read extracted file: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data, content) {
|
||||||
|
t.Errorf("extracted content mismatch: got %q, want %q", data, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractFromZip(t *testing.T) {
|
||||||
|
content := []byte("test binary content")
|
||||||
|
archive, err := createTestZip([]zipEntry{
|
||||||
|
{name: "flux-operator", content: content},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("createTestZip: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile := filepath.Join(t.TempDir(), "test.zip")
|
||||||
|
os.WriteFile(tmpFile, archive, 0o644)
|
||||||
|
|
||||||
|
destPath := filepath.Join(t.TempDir(), "flux-operator")
|
||||||
|
if err := extractFromZip(tmpFile, "flux-operator", destPath); err != nil {
|
||||||
|
t.Fatalf("extract failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(destPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read extracted file: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data, content) {
|
||||||
|
t.Errorf("content mismatch: got %q, want %q", data, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractFromZipRejectsUnsafeEntries(t *testing.T) {
|
||||||
|
content := []byte("legit content")
|
||||||
|
|
||||||
|
// Archive contains, in order:
|
||||||
|
// 1. A symlink whose basename matches the target (must be skipped).
|
||||||
|
// 2. An entry with ".." in the path (must be skipped).
|
||||||
|
// 3. An absolute-path entry (must be skipped).
|
||||||
|
// 4. A directory entry whose basename matches (must be skipped).
|
||||||
|
// 5. A legitimate regular file that must be extracted.
|
||||||
|
entries := []zipEntry{
|
||||||
|
{
|
||||||
|
name: "flux-operator",
|
||||||
|
mode: fs.ModeSymlink | 0o777,
|
||||||
|
content: []byte("/etc/passwd"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "../flux-operator",
|
||||||
|
content: []byte("malicious"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "/absolute/flux-operator",
|
||||||
|
content: []byte("malicious"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "flux-operator/",
|
||||||
|
mode: fs.ModeDir | 0o755,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bin/flux-operator",
|
||||||
|
content: content,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
archive, err := createTestZip(entries)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("createTestZip: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile := filepath.Join(t.TempDir(), "test.zip")
|
||||||
|
os.WriteFile(tmpFile, archive, 0o644)
|
||||||
|
|
||||||
|
destPath := filepath.Join(t.TempDir(), "flux-operator")
|
||||||
|
if err := extractFromZip(tmpFile, "flux-operator", destPath); err != nil {
|
||||||
|
t.Fatalf("extract failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(destPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read extracted file: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data, content) {
|
||||||
|
t.Errorf("extracted content mismatch: got %q, want %q", data, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstallExtractPath(t *testing.T) {
|
||||||
|
binaryContent := []byte("#!/bin/sh\necho nested")
|
||||||
|
|
||||||
|
// Binary is nested at "subdir/flux-operator" inside the archive.
|
||||||
|
entries := []tarEntry{
|
||||||
|
{
|
||||||
|
header: tar.Header{
|
||||||
|
Name: "subdir/flux-operator",
|
||||||
|
Typeflag: tar.TypeReg,
|
||||||
|
Mode: 0o755,
|
||||||
|
},
|
||||||
|
content: binaryContent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
archive, err := createTestTarGzMulti(entries)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum := fmt.Sprintf("sha256:%x", sha256.Sum256(archive))
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(archive)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
manifest := &plugintypes.Manifest{Name: "operator", Bin: "flux-operator"}
|
||||||
|
pv := &plugintypes.Version{Version: "0.45.0"}
|
||||||
|
plat := &plugintypes.Platform{
|
||||||
|
OS: "linux",
|
||||||
|
Arch: "amd64",
|
||||||
|
URL: server.URL + "/archive.tar.gz",
|
||||||
|
Checksum: checksum,
|
||||||
|
ExtractPath: "subdir/flux-operator",
|
||||||
|
}
|
||||||
|
|
||||||
|
installer := &Installer{HTTPClient: server.Client()}
|
||||||
|
if err := installer.Install(pluginDir, manifest, pv, plat); err != nil {
|
||||||
|
t.Fatalf("install failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary must be installed under manifest.Bin, not the extractPath.
|
||||||
|
binPath := filepath.Join(pluginDir, "flux-operator")
|
||||||
|
data, err := os.ReadFile(binPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("binary not found: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data, binaryContent) {
|
||||||
|
t.Errorf("binary content mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstallExtractPathZip(t *testing.T) {
|
||||||
|
binaryContent := []byte("#!/bin/sh\necho nested zip")
|
||||||
|
|
||||||
|
archive, err := createTestZip([]zipEntry{
|
||||||
|
{name: "pkg/bin/flux-operator", content: binaryContent},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("createTestZip: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum := fmt.Sprintf("sha256:%x", sha256.Sum256(archive))
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write(archive)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
manifest := &plugintypes.Manifest{Name: "operator", Bin: "flux-operator"}
|
||||||
|
pv := &plugintypes.Version{Version: "0.45.0"}
|
||||||
|
plat := &plugintypes.Platform{
|
||||||
|
OS: "linux",
|
||||||
|
Arch: "amd64",
|
||||||
|
URL: server.URL + "/archive.zip",
|
||||||
|
Checksum: checksum,
|
||||||
|
ExtractPath: "pkg/bin/flux-operator",
|
||||||
|
}
|
||||||
|
|
||||||
|
installer := &Installer{HTTPClient: server.Client()}
|
||||||
|
if err := installer.Install(pluginDir, manifest, pv, plat); err != nil {
|
||||||
|
t.Fatalf("install failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
binPath := filepath.Join(pluginDir, "flux-operator")
|
||||||
|
data, err := os.ReadFile(binPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("binary not found: %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(data, binaryContent) {
|
||||||
|
t.Errorf("binary content mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMatchArchiveEntry(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
entry, target string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
// Basename matching (no slash in target).
|
||||||
|
{"flux-operator", "flux-operator", true},
|
||||||
|
{"bin/flux-operator", "flux-operator", true},
|
||||||
|
{"deep/nested/flux-operator", "flux-operator", true},
|
||||||
|
{"other-binary", "flux-operator", false},
|
||||||
|
|
||||||
|
// Exact path matching (slash in target).
|
||||||
|
{"bin/flux-operator", "bin/flux-operator", true},
|
||||||
|
{"flux-operator", "bin/flux-operator", false},
|
||||||
|
{"other/flux-operator", "bin/flux-operator", false},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.entry+"_"+tc.target, func(t *testing.T) {
|
||||||
|
if got := matchArchiveEntry(tc.entry, tc.target); got != tc.want {
|
||||||
|
t.Errorf("matchArchiveEntry(%q, %q) = %v, want %v", tc.entry, tc.target, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractFromTarGzNotFound(t *testing.T) {
|
||||||
|
archive, err := createTestTarGz("other-binary", []byte("content"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create archive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile := filepath.Join(t.TempDir(), "test.tar.gz")
|
||||||
|
os.WriteFile(tmpFile, archive, 0o644)
|
||||||
|
|
||||||
|
destPath := filepath.Join(t.TempDir(), "flux-operator")
|
||||||
|
err = extractFromTarGz(tmpFile, "flux-operator", destPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
106
internal/plugin/update.go
Normal file
106
internal/plugin/update.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
plugintypes "github.com/fluxcd/flux2/v2/pkg/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SkipReasonManual = "manually installed"
|
||||||
|
SkipReasonUpToDate = "already up to date"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateResult represents the outcome of updating a single plugin.
|
||||||
|
// When an update is available, Manifest, Version and Platform are
|
||||||
|
// populated so the caller can install without re-fetching or re-resolving.
|
||||||
|
type UpdateResult struct {
|
||||||
|
// Name is the plugin name.
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// FromVersion is the currently installed version.
|
||||||
|
FromVersion string
|
||||||
|
|
||||||
|
// ToVersion is the latest available version.
|
||||||
|
ToVersion string
|
||||||
|
|
||||||
|
// Skipped is true when the update was not performed.
|
||||||
|
Skipped bool
|
||||||
|
|
||||||
|
// SkipReason explains why the update was skipped.
|
||||||
|
SkipReason string
|
||||||
|
|
||||||
|
// Manifest is the resolved plugin manifest for the update.
|
||||||
|
Manifest *plugintypes.Manifest
|
||||||
|
|
||||||
|
// Version is the resolved target version for the update.
|
||||||
|
Version *plugintypes.Version
|
||||||
|
|
||||||
|
// Platform is the resolved platform entry for the update.
|
||||||
|
Platform *plugintypes.Platform
|
||||||
|
|
||||||
|
// Err is set when the update check itself failed.
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUpdate compares the installed version against the latest in the catalog.
|
||||||
|
// Returns an UpdateResult describing what should happen. When an update is
|
||||||
|
// available, Manifest is populated so the caller can install without re-fetching.
|
||||||
|
func CheckUpdate(pluginDir string, name string, catalog *CatalogClient, goos, goarch string) UpdateResult {
|
||||||
|
receipt := ReadReceipt(pluginDir, name)
|
||||||
|
if receipt == nil {
|
||||||
|
return UpdateResult{
|
||||||
|
Name: name,
|
||||||
|
Skipped: true,
|
||||||
|
SkipReason: SkipReasonManual,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, err := catalog.FetchManifest(name)
|
||||||
|
if err != nil {
|
||||||
|
return UpdateResult{Name: name, Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
latest, err := ResolveVersion(manifest, "")
|
||||||
|
if err != nil {
|
||||||
|
return UpdateResult{Name: name, Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if receipt.Version == latest.Version {
|
||||||
|
return UpdateResult{
|
||||||
|
Name: name,
|
||||||
|
FromVersion: receipt.Version,
|
||||||
|
ToVersion: latest.Version,
|
||||||
|
Skipped: true,
|
||||||
|
SkipReason: SkipReasonUpToDate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plat, err := ResolvePlatform(latest, goos, goarch)
|
||||||
|
if err != nil {
|
||||||
|
return UpdateResult{Name: name, Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UpdateResult{
|
||||||
|
Name: name,
|
||||||
|
FromVersion: receipt.Version,
|
||||||
|
ToVersion: latest.Version,
|
||||||
|
Manifest: manifest,
|
||||||
|
Version: latest,
|
||||||
|
Platform: plat,
|
||||||
|
}
|
||||||
|
}
|
||||||
153
internal/plugin/update_test.go
Normal file
153
internal/plugin/update_test.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 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 plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckUpdateUpToDate(t *testing.T) {
|
||||||
|
manifest := `
|
||||||
|
apiVersion: cli.fluxcd.io/v1beta1
|
||||||
|
kind: Plugin
|
||||||
|
name: operator
|
||||||
|
bin: flux-operator
|
||||||
|
versions:
|
||||||
|
- version: 0.45.0
|
||||||
|
platforms:
|
||||||
|
- os: linux
|
||||||
|
arch: amd64
|
||||||
|
url: https://example.com/archive.tar.gz
|
||||||
|
checksum: sha256:abc123
|
||||||
|
`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte(manifest))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
// Write receipt with same version.
|
||||||
|
receiptData := `name: operator
|
||||||
|
version: "0.45.0"
|
||||||
|
installedAt: "2026-03-28T20:05:00Z"
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: amd64
|
||||||
|
`
|
||||||
|
os.WriteFile(filepath.Join(pluginDir, "flux-operator.yaml"), []byte(receiptData), 0o644)
|
||||||
|
|
||||||
|
catalog := &CatalogClient{
|
||||||
|
BaseURL: server.URL + "/",
|
||||||
|
HTTPClient: server.Client(),
|
||||||
|
GetEnv: func(key string) string { return "" },
|
||||||
|
}
|
||||||
|
|
||||||
|
result := CheckUpdate(pluginDir, "operator", catalog, "linux", "amd64")
|
||||||
|
if result.Err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", result.Err)
|
||||||
|
}
|
||||||
|
if !result.Skipped {
|
||||||
|
t.Error("expected skipped=true")
|
||||||
|
}
|
||||||
|
if result.SkipReason != SkipReasonUpToDate {
|
||||||
|
t.Errorf("expected %q, got %q", SkipReasonUpToDate, result.SkipReason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckUpdateAvailable(t *testing.T) {
|
||||||
|
manifest := `
|
||||||
|
apiVersion: cli.fluxcd.io/v1beta1
|
||||||
|
kind: Plugin
|
||||||
|
name: operator
|
||||||
|
bin: flux-operator
|
||||||
|
versions:
|
||||||
|
- version: 0.46.0
|
||||||
|
platforms:
|
||||||
|
- os: linux
|
||||||
|
arch: amd64
|
||||||
|
url: https://example.com/archive.tar.gz
|
||||||
|
checksum: sha256:abc123
|
||||||
|
- version: 0.45.0
|
||||||
|
platforms:
|
||||||
|
- os: linux
|
||||||
|
arch: amd64
|
||||||
|
url: https://example.com/archive.tar.gz
|
||||||
|
checksum: sha256:def456
|
||||||
|
`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte(manifest))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
receiptData := `name: operator
|
||||||
|
version: "0.45.0"
|
||||||
|
installedAt: "2026-03-28T20:05:00Z"
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: amd64
|
||||||
|
`
|
||||||
|
os.WriteFile(filepath.Join(pluginDir, "flux-operator.yaml"), []byte(receiptData), 0o644)
|
||||||
|
|
||||||
|
catalog := &CatalogClient{
|
||||||
|
BaseURL: server.URL + "/",
|
||||||
|
HTTPClient: server.Client(),
|
||||||
|
GetEnv: func(key string) string { return "" },
|
||||||
|
}
|
||||||
|
|
||||||
|
result := CheckUpdate(pluginDir, "operator", catalog, "linux", "amd64")
|
||||||
|
if result.Err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", result.Err)
|
||||||
|
}
|
||||||
|
if result.Skipped {
|
||||||
|
t.Error("expected skipped=false")
|
||||||
|
}
|
||||||
|
if result.FromVersion != "0.45.0" {
|
||||||
|
t.Errorf("expected from '0.45.0', got %q", result.FromVersion)
|
||||||
|
}
|
||||||
|
if result.ToVersion != "0.46.0" {
|
||||||
|
t.Errorf("expected to '0.46.0', got %q", result.ToVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckUpdateManualInstall(t *testing.T) {
|
||||||
|
pluginDir := t.TempDir()
|
||||||
|
|
||||||
|
// No receipt — manually installed.
|
||||||
|
catalog := &CatalogClient{
|
||||||
|
BaseURL: "https://example.com/",
|
||||||
|
HTTPClient: http.DefaultClient,
|
||||||
|
GetEnv: func(key string) string { return "" },
|
||||||
|
}
|
||||||
|
|
||||||
|
result := CheckUpdate(pluginDir, "operator", catalog, "linux", "amd64")
|
||||||
|
if result.Err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", result.Err)
|
||||||
|
}
|
||||||
|
if !result.Skipped {
|
||||||
|
t.Error("expected skipped=true")
|
||||||
|
}
|
||||||
|
if result.SkipReason != SkipReasonManual {
|
||||||
|
t.Errorf("expected 'manually installed', got %q", result.SkipReason)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- https://github.com/fluxcd/helm-controller/releases/download/v1.4.5/helm-controller.crds.yaml
|
- https://github.com/fluxcd/helm-controller/releases/download/v1.5.4/helm-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/helm-controller/releases/download/v1.4.5/helm-controller.deployment.yaml
|
- https://github.com/fluxcd/helm-controller/releases/download/v1.5.4/helm-controller.deployment.yaml
|
||||||
- account.yaml
|
- account.yaml
|
||||||
transformers:
|
transformers:
|
||||||
- labels.yaml
|
- labels.yaml
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.0.4/image-automation-controller.crds.yaml
|
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.1.2/image-automation-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.0.4/image-automation-controller.deployment.yaml
|
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.1.2/image-automation-controller.deployment.yaml
|
||||||
- account.yaml
|
- account.yaml
|
||||||
transformers:
|
transformers:
|
||||||
- labels.yaml
|
- labels.yaml
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.0.4/image-reflector-controller.crds.yaml
|
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.1.1/image-reflector-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.0.4/image-reflector-controller.deployment.yaml
|
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.1.1/image-reflector-controller.deployment.yaml
|
||||||
- account.yaml
|
- account.yaml
|
||||||
transformers:
|
transformers:
|
||||||
- labels.yaml
|
- labels.yaml
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.7.3/kustomize-controller.crds.yaml
|
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.4/kustomize-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.7.3/kustomize-controller.deployment.yaml
|
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.4/kustomize-controller.deployment.yaml
|
||||||
- account.yaml
|
- account.yaml
|
||||||
transformers:
|
transformers:
|
||||||
- labels.yaml
|
- labels.yaml
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- https://github.com/fluxcd/notification-controller/releases/download/v1.7.5/notification-controller.crds.yaml
|
- https://github.com/fluxcd/notification-controller/releases/download/v1.8.4/notification-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/notification-controller/releases/download/v1.7.5/notification-controller.deployment.yaml
|
- https://github.com/fluxcd/notification-controller/releases/download/v1.8.4/notification-controller.deployment.yaml
|
||||||
- account.yaml
|
- account.yaml
|
||||||
transformers:
|
transformers:
|
||||||
- labels.yaml
|
- labels.yaml
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- https://github.com/fluxcd/source-controller/releases/download/v1.7.4/source-controller.crds.yaml
|
- https://github.com/fluxcd/source-controller/releases/download/v1.8.3/source-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/source-controller/releases/download/v1.7.4/source-controller.deployment.yaml
|
- https://github.com/fluxcd/source-controller/releases/download/v1.8.3/source-controller.deployment.yaml
|
||||||
- account.yaml
|
- account.yaml
|
||||||
transformers:
|
transformers:
|
||||||
- labels.yaml
|
- labels.yaml
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- https://github.com/fluxcd/source-watcher/releases/download/v2.0.3/source-watcher.crds.yaml
|
- https://github.com/fluxcd/source-watcher/releases/download/v2.1.1/source-watcher.crds.yaml
|
||||||
- https://github.com/fluxcd/source-watcher/releases/download/v2.0.3/source-watcher.deployment.yaml
|
- https://github.com/fluxcd/source-watcher/releases/download/v2.1.1/source-watcher.deployment.yaml
|
||||||
- account.yaml
|
- account.yaml
|
||||||
transformers:
|
transformers:
|
||||||
- labels.yaml
|
- labels.yaml
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- https://github.com/fluxcd/source-controller/releases/download/v1.7.4/source-controller.crds.yaml
|
- https://github.com/fluxcd/source-controller/releases/download/v1.8.3/source-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.7.3/kustomize-controller.crds.yaml
|
- https://github.com/fluxcd/kustomize-controller/releases/download/v1.8.4/kustomize-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/helm-controller/releases/download/v1.4.5/helm-controller.crds.yaml
|
- https://github.com/fluxcd/helm-controller/releases/download/v1.5.4/helm-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/notification-controller/releases/download/v1.7.5/notification-controller.crds.yaml
|
- https://github.com/fluxcd/notification-controller/releases/download/v1.8.4/notification-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.0.4/image-reflector-controller.crds.yaml
|
- https://github.com/fluxcd/image-reflector-controller/releases/download/v1.1.1/image-reflector-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.0.4/image-automation-controller.crds.yaml
|
- https://github.com/fluxcd/image-automation-controller/releases/download/v1.1.2/image-automation-controller.crds.yaml
|
||||||
- https://github.com/fluxcd/source-watcher/releases/download/v2.0.3/source-watcher.crds.yaml
|
- https://github.com/fluxcd/source-watcher/releases/download/v2.1.1/source-watcher.crds.yaml
|
||||||
|
|||||||
@@ -169,19 +169,19 @@ func BuildWithRoot(root, base string) ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("%s not found", konfig.DefaultKustomizationFileName())
|
return nil, fmt.Errorf("%s not found", konfig.DefaultKustomizationFileName())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(hidde): work around for a bug in kustomize causing it to
|
// Convert absolute paths to relative when possible, for kustomize
|
||||||
// not properly handle absolute paths on Windows.
|
// compatibility. If filepath.Rel fails (e.g. paths on different
|
||||||
// Convert the path to a relative path to the working directory
|
// Windows drives), keep the absolute path — kustomize handles
|
||||||
// as a temporary fix:
|
// absolute paths correctly since go-getter was removed.
|
||||||
// https://github.com/kubernetes-sigs/kustomize/issues/2789
|
// See: https://github.com/kubernetes-sigs/kustomize/issues/2789
|
||||||
|
// https://github.com/fluxcd/flux2/issues/1153
|
||||||
if filepath.IsAbs(base) {
|
if filepath.IsAbs(base) {
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
base, err = filepath.Rel(wd, base)
|
if relBase, err := filepath.Rel(wd, base); err == nil {
|
||||||
if err != nil {
|
base = relBase
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ const (
|
|||||||
KnownHostsSecretKey = "known_hosts"
|
KnownHostsSecretKey = "known_hosts"
|
||||||
BearerTokenKey = "bearerToken"
|
BearerTokenKey = "bearerToken"
|
||||||
TrustPolicyKey = "trustpolicy.json"
|
TrustPolicyKey = "trustpolicy.json"
|
||||||
|
TokenSecretKey = "token"
|
||||||
|
EmailSecretKey = "email"
|
||||||
|
AudienceSecretKey = "audience"
|
||||||
|
|
||||||
|
// WebhookURLAnnotation is the annotation key for the computed webhook URL.
|
||||||
|
WebhookURLAnnotation = "notification.toolkit.fluxcd.io/webhook"
|
||||||
|
|
||||||
// Deprecated: Replaced by CACrtSecretKey, but kept for backwards
|
// Deprecated: Replaced by CACrtSecretKey, but kept for backwards
|
||||||
// compatibility with deprecated TLS flags.
|
// compatibility with deprecated TLS flags.
|
||||||
@@ -82,6 +88,13 @@ type Options struct {
|
|||||||
GitHubAppInstallationID string
|
GitHubAppInstallationID string
|
||||||
GitHubAppPrivateKey string
|
GitHubAppPrivateKey string
|
||||||
GitHubAppBaseURL string
|
GitHubAppBaseURL string
|
||||||
|
|
||||||
|
// Receiver options
|
||||||
|
ReceiverType string
|
||||||
|
Token string
|
||||||
|
Hostname string
|
||||||
|
EmailClaim string
|
||||||
|
AudienceClaim string
|
||||||
}
|
}
|
||||||
|
|
||||||
type VerificationCrt struct {
|
type VerificationCrt struct {
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ package sourcesecret
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -260,6 +263,59 @@ func GenerateGitHubApp(options Options) (*manifestgen.Manifest, error) {
|
|||||||
return secretToManifest(secret, options)
|
return secretToManifest(secret, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateReceiver(options Options) (*manifestgen.Manifest, error) {
|
||||||
|
token := options.Token
|
||||||
|
if token == "" {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate random token: %w", err)
|
||||||
|
}
|
||||||
|
token = hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Hostname == "" {
|
||||||
|
return nil, fmt.Errorf("hostname is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the webhook path using the same algorithm as notification-controller.
|
||||||
|
// See: github.com/fluxcd/notification-controller/api/v1.Receiver.GetWebhookPath
|
||||||
|
digest := sha256.Sum256([]byte(token + options.Name + options.Namespace))
|
||||||
|
webhookPath := fmt.Sprintf("/hook/%x", digest)
|
||||||
|
webhookURL := fmt.Sprintf("https://%s%s", options.Hostname, webhookPath)
|
||||||
|
|
||||||
|
secret := &corev1.Secret{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
APIVersion: "v1",
|
||||||
|
Kind: "Secret",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: options.Name,
|
||||||
|
Namespace: options.Namespace,
|
||||||
|
Labels: options.Labels,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
WebhookURLAnnotation: webhookURL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
StringData: map[string]string{
|
||||||
|
TokenSecretKey: token,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.ReceiverType == "gcr" {
|
||||||
|
if options.EmailClaim == "" {
|
||||||
|
return nil, fmt.Errorf("email-claim is required for gcr receiver type")
|
||||||
|
}
|
||||||
|
secret.StringData[EmailSecretKey] = options.EmailClaim
|
||||||
|
if options.AudienceClaim != "" {
|
||||||
|
secret.StringData[AudienceSecretKey] = options.AudienceClaim
|
||||||
|
} else {
|
||||||
|
secret.StringData[AudienceSecretKey] = webhookURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return secretToManifest(secret, options)
|
||||||
|
}
|
||||||
|
|
||||||
func LoadKeyPairFromPath(path, password string) (*ssh.KeyPair, error) {
|
func LoadKeyPairFromPath(path, password string) (*ssh.KeyPair, error) {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user