mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-04-10 18:58:47 +02:00
Compare commits
15 Commits
renovate/g
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dd9075a72 | ||
|
|
38a3826df8 | ||
|
|
cd8a25c195 | ||
|
|
7b5b6a8744 | ||
|
|
3a6083cb2d | ||
|
|
fb82b16b2d | ||
|
|
ae10c0c6c3 | ||
|
|
9e80e058e7 | ||
|
|
4220eddbf3 | ||
|
|
422ef230fa | ||
|
|
1fde0d075f | ||
|
|
f86cd078fc | ||
|
|
c3b498c2ae | ||
|
|
ad310c2452 | ||
|
|
6743cdf9cc |
2
.github/workflows/bump-major-version.yaml
vendored
2
.github/workflows/bump-major-version.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
2
.github/workflows/cherry-pick-single.yml
vendored
2
.github/workflows/cherry-pick-single.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
2
.github/workflows/cherry-pick.yml
vendored
2
.github/workflows/cherry-pick.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
22
.github/workflows/ci-build.yaml
vendored
22
.github/workflows/ci-build.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout code
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout code
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout code
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Create checkout directory
|
||||
@@ -226,7 +226,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Create checkout directory
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout code
|
||||
@@ -357,7 +357,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout code
|
||||
@@ -415,7 +415,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout code
|
||||
@@ -496,7 +496,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
@@ -632,7 +632,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- run: |
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
2
.github/workflows/image-reuse.yaml
vendored
2
.github/workflows/image-reuse.yaml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
2
.github/workflows/image.yaml
vendored
2
.github/workflows/image.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
2
.github/workflows/init-release.yaml
vendored
2
.github/workflows/init-release.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
2
.github/workflows/pr-title-check.yml
vendored
2
.github/workflows/pr-title-check.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout code
|
||||
|
||||
18
.github/workflows/renovate.yaml
vendored
18
.github/workflows/renovate.yaml
vendored
@@ -16,11 +16,23 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.repository == 'argoproj/argo-cd'
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
- name: Harden the runner (Block unknown outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
egress-policy: block
|
||||
disable-sudo-and-containers: "false" # renovatebot runs in `docker run`
|
||||
allowed-endpoints: >
|
||||
github.com:443
|
||||
api.github.com:443
|
||||
raw.githubusercontent.com:443
|
||||
release-assets.githubusercontent.com:443
|
||||
ghcr.io:443
|
||||
pkg-containers.githubusercontent.com:443
|
||||
hub.docker.com:443
|
||||
proxy.golang.org:443
|
||||
nodejs.org:443
|
||||
pypi.org:443
|
||||
|
||||
- name: Get token
|
||||
id: get_token
|
||||
|
||||
2
.github/workflows/scorecard.yaml
vendored
2
.github/workflows/scorecard.yaml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
9
.github/workflows/stale.yaml
vendored
9
.github/workflows/stale.yaml
vendored
@@ -16,11 +16,14 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
- name: Harden the runner (Block unknown outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
egress-policy: block
|
||||
disable-sudo-and-containers: "true"
|
||||
allowed-endpoints: >
|
||||
api.github.com:443
|
||||
|
||||
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/update-snyk.yaml
vendored
2
.github/workflows/update-snyk.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
if: ${{ vars.disable_harden_runner != 'true' }}
|
||||
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
|
||||
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
agent-enabled: "false"
|
||||
|
||||
@@ -4,6 +4,7 @@ WORKDIR /app/ui
|
||||
|
||||
COPY ui /app/ui
|
||||
|
||||
RUN npm install -g corepack@0.34.6 && corepack enable && pnpm install
|
||||
RUN npm install -g corepack@0.34.6 && corepack enable && pnpm install --frozen-lockfile
|
||||
|
||||
ENTRYPOINT ["pnpm", "start"]
|
||||
|
||||
ENTRYPOINT ["pnpm", "start"]
|
||||
11
Makefile
11
Makefile
@@ -662,8 +662,17 @@ install-go-tools-local:
|
||||
dep-ui: test-tools-image
|
||||
$(call run-in-test-client,make dep-ui-local)
|
||||
|
||||
.PHONY: dep-ui-local
|
||||
dep-ui-local:
|
||||
cd ui && pnpm install
|
||||
cd ui && pnpm install --frozen-lockfile
|
||||
|
||||
.PHONY: run-pnpm
|
||||
run-pnpm: test-tools-image
|
||||
$(call run-in-test-client,make 'PNPM_COMMAND=$(PNPM_COMMAND)' run-pnpm-local)
|
||||
|
||||
.PHONY: run-pnpm-local
|
||||
run-pnpm-local:
|
||||
cd ui && pnpm $(PNPM_COMMAND)
|
||||
|
||||
start-test-k8s:
|
||||
go run ./hack/k8s
|
||||
|
||||
2
Tiltfile
2
Tiltfile
@@ -275,7 +275,7 @@ docker_build(
|
||||
only=['ui'],
|
||||
live_update=[
|
||||
sync('ui', '/app/ui'),
|
||||
run('sh -c "cd /app/ui && pnpm install"', trigger=['/app/ui/package.json', '/app/ui/pnpm-lock.yaml']),
|
||||
run('sh -c "cd /app/ui && pnpm install --frozen-lockfile"', trigger=['/app/ui/package.json', '/app/ui/pnpm-lock.yaml']),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
4
assets/swagger.json
generated
4
assets/swagger.json
generated
@@ -9727,6 +9727,10 @@
|
||||
"username": {
|
||||
"type": "string",
|
||||
"title": "Username contains the user name used for authenticating at the remote repository"
|
||||
},
|
||||
"webhookManifestCacheWarmDisabled": {
|
||||
"description": "WebhookManifestCacheWarmDisabled disables manifest cache warming during webhook processing for this repository.\nWhen set, webhook handlers will only trigger reconciliation for affected applications and skip Redis cache\noperations for unaffected ones. Recommended for large monorepos with plain YAML manifests.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -149,6 +149,7 @@ func NewGenRepoSpecCommand() *cobra.Command {
|
||||
repoOpts.Repo.EnableOCI = repoOpts.EnableOci
|
||||
repoOpts.Repo.UseAzureWorkloadIdentity = repoOpts.UseAzureWorkloadIdentity
|
||||
repoOpts.Repo.InsecureOCIForceHttp = repoOpts.InsecureOCIForceHTTP
|
||||
repoOpts.Repo.WebhookManifestCacheWarmDisabled = repoOpts.WebhookManifestCacheWarmDisabled
|
||||
|
||||
if repoOpts.Repo.Type == "helm" && repoOpts.Repo.Name == "" {
|
||||
errors.CheckError(stderrors.New("must specify --name for repos of type 'helm'"))
|
||||
|
||||
@@ -192,6 +192,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
repoOpts.Repo.ForceHttpBasicAuth = repoOpts.ForceHttpBasicAuth
|
||||
repoOpts.Repo.UseAzureWorkloadIdentity = repoOpts.UseAzureWorkloadIdentity
|
||||
repoOpts.Repo.Depth = repoOpts.Depth
|
||||
repoOpts.Repo.WebhookManifestCacheWarmDisabled = repoOpts.WebhookManifestCacheWarmDisabled
|
||||
|
||||
if repoOpts.Repo.Type == "helm" && repoOpts.Repo.Name == "" {
|
||||
errors.Fatal(errors.ErrorGeneric, "Must specify --name for repos of type 'helm'")
|
||||
|
||||
@@ -8,26 +8,27 @@ import (
|
||||
)
|
||||
|
||||
type RepoOptions struct {
|
||||
Repo appsv1.Repository
|
||||
Upsert bool
|
||||
SshPrivateKeyPath string //nolint:revive //FIXME(var-naming)
|
||||
InsecureOCIForceHTTP bool
|
||||
InsecureIgnoreHostKey bool
|
||||
InsecureSkipServerVerification bool
|
||||
TlsClientCertPath string //nolint:revive //FIXME(var-naming)
|
||||
TlsClientCertKeyPath string //nolint:revive //FIXME(var-naming)
|
||||
EnableLfs bool
|
||||
EnableOci bool
|
||||
GithubAppId int64
|
||||
GithubAppInstallationId int64
|
||||
GithubAppPrivateKeyPath string
|
||||
GitHubAppEnterpriseBaseURL string
|
||||
Proxy string
|
||||
NoProxy string
|
||||
GCPServiceAccountKeyPath string
|
||||
ForceHttpBasicAuth bool //nolint:revive //FIXME(var-naming)
|
||||
UseAzureWorkloadIdentity bool
|
||||
Depth int64
|
||||
Repo appsv1.Repository
|
||||
Upsert bool
|
||||
SshPrivateKeyPath string //nolint:revive //FIXME(var-naming)
|
||||
InsecureOCIForceHTTP bool
|
||||
InsecureIgnoreHostKey bool
|
||||
InsecureSkipServerVerification bool
|
||||
TlsClientCertPath string //nolint:revive //FIXME(var-naming)
|
||||
TlsClientCertKeyPath string //nolint:revive //FIXME(var-naming)
|
||||
EnableLfs bool
|
||||
EnableOci bool
|
||||
GithubAppId int64
|
||||
GithubAppInstallationId int64
|
||||
GithubAppPrivateKeyPath string
|
||||
GitHubAppEnterpriseBaseURL string
|
||||
Proxy string
|
||||
NoProxy string
|
||||
GCPServiceAccountKeyPath string
|
||||
ForceHttpBasicAuth bool //nolint:revive //FIXME(var-naming)
|
||||
UseAzureWorkloadIdentity bool
|
||||
Depth int64
|
||||
WebhookManifestCacheWarmDisabled bool
|
||||
}
|
||||
|
||||
func AddRepoFlags(command *cobra.Command, opts *RepoOptions) {
|
||||
@@ -55,4 +56,5 @@ func AddRepoFlags(command *cobra.Command, opts *RepoOptions) {
|
||||
command.Flags().BoolVar(&opts.UseAzureWorkloadIdentity, "use-azure-workload-identity", false, "whether to use azure workload identity for authentication")
|
||||
command.Flags().BoolVar(&opts.InsecureOCIForceHTTP, "insecure-oci-force-http", false, "Use http when accessing an OCI repository")
|
||||
command.Flags().Int64Var(&opts.Depth, "depth", 0, "Specify a custom depth for git clone operations. Unless specified, a full clone is performed using the depth of 0")
|
||||
command.Flags().BoolVar(&opts.WebhookManifestCacheWarmDisabled, "webhook-manifest-cache-warm-disabled", false, "disable manifest cache warming during webhook processing for this repository (recommended for large monorepos with plain YAML manifests)")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/gitops-engine/pkg/sync/hook"
|
||||
"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
|
||||
log "github.com/sirupsen/logrus"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/rest"
|
||||
@@ -103,6 +104,7 @@ func (ctrl *ApplicationController) executeHooks(hookType HookType, app *appv1.Ap
|
||||
revisions = append(revisions, src.TargetRevision)
|
||||
}
|
||||
|
||||
// Fetch target objects from Git to know which hooks should exist
|
||||
targets, _, _, err := ctrl.appStateManager.GetRepoObjs(context.Background(), app, app.Spec.GetSources(), appLabelKey, revisions, false, false, false, proj, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -125,14 +127,14 @@ func (ctrl *ApplicationController) executeHooks(hookType HookType, app *appv1.Ap
|
||||
if !isHookOfType(obj, hookType) {
|
||||
continue
|
||||
}
|
||||
if runningHook := runningHooks[kube.GetResourceKey(obj)]; runningHook == nil {
|
||||
if _, alreadyExists := runningHooks[kube.GetResourceKey(obj)]; !alreadyExists {
|
||||
expectedHook[kube.GetResourceKey(obj)] = obj
|
||||
}
|
||||
}
|
||||
|
||||
// Create hooks that don't exist yet
|
||||
createdCnt := 0
|
||||
for _, obj := range expectedHook {
|
||||
for key, obj := range expectedHook {
|
||||
// Add app instance label so the hook can be tracked and cleaned up
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
@@ -141,8 +143,13 @@ func (ctrl *ApplicationController) executeHooks(hookType HookType, app *appv1.Ap
|
||||
labels[appLabelKey] = app.InstanceName(ctrl.namespace)
|
||||
obj.SetLabels(labels)
|
||||
|
||||
logCtx.Infof("Creating %s hook resource: %s", hookType, key)
|
||||
_, err = ctrl.kubectl.CreateResource(context.Background(), config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), obj, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
logCtx.Warnf("Hook resource %s already exists, skipping", key)
|
||||
continue
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
createdCnt++
|
||||
@@ -163,7 +170,8 @@ func (ctrl *ApplicationController) executeHooks(hookType HookType, app *appv1.Ap
|
||||
progressingHooksCount := 0
|
||||
var failedHooks []string
|
||||
var failedHookObjects []*unstructured.Unstructured
|
||||
for _, obj := range runningHooks {
|
||||
|
||||
for key, obj := range runningHooks {
|
||||
hookHealth, err := health.GetResourceHealth(obj, healthOverrides)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -180,12 +188,17 @@ func (ctrl *ApplicationController) executeHooks(hookType HookType, app *appv1.Ap
|
||||
Status: health.HealthStatusHealthy,
|
||||
}
|
||||
}
|
||||
|
||||
switch hookHealth.Status {
|
||||
case health.HealthStatusProgressing:
|
||||
logCtx.Debugf("Hook %s is progressing", key)
|
||||
progressingHooksCount++
|
||||
case health.HealthStatusDegraded:
|
||||
logCtx.Warnf("Hook %s is degraded: %s", key, hookHealth.Message)
|
||||
failedHooks = append(failedHooks, fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()))
|
||||
failedHookObjects = append(failedHookObjects, obj)
|
||||
case health.HealthStatusHealthy:
|
||||
logCtx.Debugf("Hook %s is healthy", key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +207,7 @@ func (ctrl *ApplicationController) executeHooks(hookType HookType, app *appv1.Ap
|
||||
logCtx.Infof("Deleting %d failed %s hook(s) to allow retry", len(failedHookObjects), hookType)
|
||||
for _, obj := range failedHookObjects {
|
||||
err = ctrl.kubectl.DeleteResource(context.Background(), config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
logCtx.WithError(err).Warnf("Failed to delete failed hook %s/%s", obj.GetNamespace(), obj.GetName())
|
||||
}
|
||||
}
|
||||
@@ -241,6 +254,10 @@ func (ctrl *ApplicationController) cleanupHooks(hookType HookType, liveObjs map[
|
||||
hooks = append(hooks, obj)
|
||||
}
|
||||
|
||||
if len(hooks) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Process hooks for deletion
|
||||
for _, obj := range hooks {
|
||||
deletePolicies := hook.DeletePolicies(obj)
|
||||
@@ -267,7 +284,7 @@ func (ctrl *ApplicationController) cleanupHooks(hookType HookType, liveObjs map[
|
||||
}
|
||||
logCtx.Infof("Deleting %s hook %s/%s", hookType, obj.GetNamespace(), obj.GetName())
|
||||
err = ctrl.kubectl.DeleteResource(context.Background(), config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
if err != nil && !apierrors.IsNotFound(err) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ package controller
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/gitops-engine/pkg/utils/kube"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func TestIsHookOfType(t *testing.T) {
|
||||
@@ -312,3 +314,174 @@ func TestMultiHookOfType(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteHooksAlreadyExistsLogic(t *testing.T) {
|
||||
newObj := func(name string, annot map[string]string) *unstructured.Unstructured {
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetGroupVersionKind(schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "Job"})
|
||||
obj.SetName(name)
|
||||
obj.SetNamespace("default")
|
||||
obj.SetAnnotations(annot)
|
||||
return obj
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hookType []HookType
|
||||
targetAnnot map[string]string
|
||||
liveAnnot map[string]string // nil -> object doesn't exist in cluster
|
||||
expectCreated bool
|
||||
}{
|
||||
// PRE DELETE TESTS
|
||||
{
|
||||
name: "PreDelete (argocd): Not in cluster - should be created",
|
||||
hookType: []HookType{PreDeleteHookType},
|
||||
targetAnnot: map[string]string{"argocd.argoproj.io/hook": "PreDelete"},
|
||||
liveAnnot: nil,
|
||||
expectCreated: true,
|
||||
},
|
||||
{
|
||||
name: "PreDelete (helm): Not in cluster - should be created",
|
||||
hookType: []HookType{PreDeleteHookType},
|
||||
targetAnnot: map[string]string{"helm.sh/hook": "pre-delete"},
|
||||
liveAnnot: nil,
|
||||
expectCreated: true,
|
||||
},
|
||||
{
|
||||
name: "PreDelete (argocd): Already exists - should be skipped",
|
||||
hookType: []HookType{PreDeleteHookType},
|
||||
targetAnnot: map[string]string{"argocd.argoproj.io/hook": "PreDelete"},
|
||||
liveAnnot: map[string]string{"argocd.argoproj.io/hook": "PreDelete"},
|
||||
expectCreated: false,
|
||||
},
|
||||
{
|
||||
name: "PreDelete (argocd): Already exists - should be skipped",
|
||||
hookType: []HookType{PreDeleteHookType},
|
||||
targetAnnot: map[string]string{"helm.sh/hook": "pre-delete"},
|
||||
liveAnnot: map[string]string{"helm.sh/hook": "pre-delete"},
|
||||
expectCreated: false,
|
||||
},
|
||||
{
|
||||
name: "PreDelete (helm+argocd): One of two already exists - should be skipped",
|
||||
hookType: []HookType{PreDeleteHookType},
|
||||
targetAnnot: map[string]string{"helm.sh/hook": "pre-delete", "argocd.argoproj.io/hook": "PreDelete"},
|
||||
liveAnnot: map[string]string{"helm.sh/hook": "pre-delete"},
|
||||
expectCreated: false,
|
||||
},
|
||||
{
|
||||
name: "PreDelete (helm+argocd): One of two already exists - should be skipped",
|
||||
hookType: []HookType{PreDeleteHookType},
|
||||
targetAnnot: map[string]string{"helm.sh/hook": "pre-delete", "argocd.argoproj.io/hook": "PreDelete"},
|
||||
liveAnnot: map[string]string{"argocd.argoproj.io/hook": "PreDelete"},
|
||||
expectCreated: false,
|
||||
},
|
||||
// POST DELETE TESTS
|
||||
{
|
||||
name: "PostDelete (argocd): Not in cluster - should be created",
|
||||
hookType: []HookType{PostDeleteHookType},
|
||||
targetAnnot: map[string]string{"argocd.argoproj.io/hook": "PostDelete"},
|
||||
liveAnnot: nil,
|
||||
expectCreated: true,
|
||||
},
|
||||
{
|
||||
name: "PostDelete (helm): Not in cluster - should be created",
|
||||
hookType: []HookType{PostDeleteHookType},
|
||||
targetAnnot: map[string]string{"helm.sh/hook": "post-delete"},
|
||||
liveAnnot: nil,
|
||||
expectCreated: true,
|
||||
},
|
||||
{
|
||||
name: "PostDelete (argocd): Already exists - should be skipped",
|
||||
hookType: []HookType{PostDeleteHookType},
|
||||
targetAnnot: map[string]string{"argocd.argoproj.io/hook": "PostDelete"},
|
||||
liveAnnot: map[string]string{"argocd.argoproj.io/hook": "PostDelete"},
|
||||
expectCreated: false,
|
||||
},
|
||||
{
|
||||
name: "PostDelete (helm): Already exists - should be skipped",
|
||||
hookType: []HookType{PostDeleteHookType},
|
||||
targetAnnot: map[string]string{"helm.sh/hook": "post-delete"},
|
||||
liveAnnot: map[string]string{"helm.sh/hook": "post-delete"},
|
||||
expectCreated: false,
|
||||
},
|
||||
{
|
||||
name: "PostDelete (helm+argocd): Already exists - should be skipped",
|
||||
hookType: []HookType{PostDeleteHookType},
|
||||
targetAnnot: map[string]string{"helm.sh/hook": "post-delete", "argocd.argoproj.io/hook": "PostDelete"},
|
||||
liveAnnot: map[string]string{"helm.sh/hook": "post-delete", "argocd.argoproj.io/hook": "PostDelete"},
|
||||
expectCreated: false,
|
||||
},
|
||||
{
|
||||
name: "PostDelete (helm+argocd): One of two already exists - should be skipped",
|
||||
hookType: []HookType{PostDeleteHookType},
|
||||
targetAnnot: map[string]string{"helm.sh/hook": "post-delete", "argocd.argoproj.io/hook": "PostDelete"},
|
||||
liveAnnot: map[string]string{"helm.sh/hook": "post-delete"},
|
||||
expectCreated: false,
|
||||
},
|
||||
{
|
||||
name: "PostDelete (helm+argocd): One of two already exists - should be skipped",
|
||||
hookType: []HookType{PostDeleteHookType},
|
||||
targetAnnot: map[string]string{"helm.sh/hook": "post-delete", "argocd.argoproj.io/hook": "PostDelete"},
|
||||
liveAnnot: map[string]string{"argocd.argoproj.io/hook": "PostDelete"},
|
||||
expectCreated: false,
|
||||
},
|
||||
// MULTI HOOK TESTS - SKIP LOGIC
|
||||
{
|
||||
name: "Multi-hook (argocd): Target is (Pre,Post), Cluster has (Pre,Post) - should be skipped",
|
||||
hookType: []HookType{PreDeleteHookType, PostDeleteHookType},
|
||||
targetAnnot: map[string]string{"argocd.argoproj.io/hook": "PreDelete,PostDelete"},
|
||||
liveAnnot: map[string]string{"argocd.argoproj.io/hook": "PreDelete,PostDelete"},
|
||||
expectCreated: false,
|
||||
},
|
||||
{
|
||||
name: "Multi-hook (helm): Target is (Pre,Post), Cluster has (Pre,Post) - should be skipped",
|
||||
hookType: []HookType{PreDeleteHookType, PostDeleteHookType},
|
||||
targetAnnot: map[string]string{"helm.sh/hook": "post-delete,pre-delete"},
|
||||
liveAnnot: map[string]string{"helm.sh/hook": "post-delete,pre-delete"},
|
||||
expectCreated: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
targetObj := newObj("my-hook", tt.targetAnnot)
|
||||
targetKey := kube.GetResourceKey(targetObj)
|
||||
|
||||
liveObjs := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
if tt.liveAnnot != nil {
|
||||
liveObjs[targetKey] = newObj("my-hook", tt.liveAnnot)
|
||||
}
|
||||
|
||||
runningHooks := map[kube.ResourceKey]*unstructured.Unstructured{}
|
||||
for key, obj := range liveObjs {
|
||||
for _, hookType := range tt.hookType {
|
||||
if isHookOfType(obj, hookType) {
|
||||
runningHooks[key] = obj
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expectedHooksToCreate := map[kube.ResourceKey]*unstructured.Unstructured{}
|
||||
targets := []*unstructured.Unstructured{targetObj}
|
||||
|
||||
for _, obj := range targets {
|
||||
for _, hookType := range tt.hookType {
|
||||
if !isHookOfType(obj, hookType) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
objKey := kube.GetResourceKey(obj)
|
||||
if _, alreadyExists := runningHooks[objKey]; !alreadyExists {
|
||||
expectedHooksToCreate[objKey] = obj
|
||||
}
|
||||
}
|
||||
|
||||
if tt.expectCreated {
|
||||
assert.NotEmpty(t, expectedHooksToCreate, "Expected hook to be marked for creation")
|
||||
} else {
|
||||
assert.Empty(t, expectedHooksToCreate, "Expected hook to be skipped (already exists)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,18 +41,13 @@ import (
|
||||
"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
|
||||
appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/v3/util/db"
|
||||
"github.com/argoproj/argo-cd/v3/util/env"
|
||||
"github.com/argoproj/argo-cd/v3/util/gpg"
|
||||
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
"github.com/argoproj/argo-cd/v3/util/stats"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCompareStateRepo = errors.New("failed to get repo objects")
|
||||
|
||||
processManifestGeneratePathsEnabled = env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_PROCESS_MANIFEST_GENERATE_PATHS", true)
|
||||
)
|
||||
var ErrCompareStateRepo = errors.New("failed to get repo objects")
|
||||
|
||||
type resourceInfoProviderStub struct{}
|
||||
|
||||
@@ -75,7 +70,7 @@ type managedResource struct {
|
||||
|
||||
// AppStateManager defines methods which allow to compare application spec and actual application state.
|
||||
type AppStateManager interface {
|
||||
CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache, noRevisionCache bool, localObjects []string, hasMultipleSources bool) (*comparisonResult, error)
|
||||
CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localObjects []string, hasMultipleSources bool) (*comparisonResult, error)
|
||||
SyncAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, state *v1alpha1.OperationState)
|
||||
GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error)
|
||||
}
|
||||
@@ -261,14 +256,7 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
|
||||
appNamespace := app.Spec.Destination.Namespace
|
||||
apiVersions := argo.APIResourcesToStrings(apiResources, true)
|
||||
|
||||
updateRevisions := processManifestGeneratePathsEnabled &&
|
||||
// updating revisions result is not required if automated sync is not enabled
|
||||
app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.Automated != nil &&
|
||||
// using updating revisions gains performance only if manifest generation is required.
|
||||
// just reading pre-generated manifests is comparable to updating revisions time-wise
|
||||
app.Status.SourceType != v1alpha1.ApplicationSourceTypeDirectory
|
||||
|
||||
if updateRevisions && repo.Depth == 0 && syncedRevision != "" && !source.IsRef() && keyManifestGenerateAnnotationExists && keyManifestGenerateAnnotationVal != "" && (syncedRevision != revision || app.Spec.HasMultipleSources()) {
|
||||
if repo.Depth == 0 && syncedRevision != "" && !source.IsRef() && keyManifestGenerateAnnotationExists && keyManifestGenerateAnnotationVal != "" && (syncedRevision != revision || app.Spec.HasMultipleSources()) {
|
||||
// Validate the manifest-generate-path annotation to avoid generating manifests if it has not changed.
|
||||
updateRevisionResult, err := repoClient.UpdateRevisionForPaths(ctx, &apiclient.UpdateRevisionForPathsRequest{
|
||||
Repo: repo,
|
||||
@@ -367,7 +355,7 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
|
||||
}
|
||||
|
||||
// ResolveGitRevision will resolve the given revision to a full commit SHA. Only works for git.
|
||||
func (m *appStateManager) ResolveGitRevision(repoURL, revision string) (string, error) {
|
||||
func (m *appStateManager) ResolveGitRevision(repoURL string, revision string) (string, error) {
|
||||
conn, repoClient, err := m.repoClientset.NewRepoServerClient()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to connect to repo server: %w", err)
|
||||
@@ -568,7 +556,7 @@ func partitionTargetObjsForSync(targetObjs []*unstructured.Unstructured) (syncOb
|
||||
// CompareAppState compares application git state to the live app state, using the specified
|
||||
// revision and supplied source. If revision or overrides are empty, then compares against
|
||||
// revision and overrides in the app spec.
|
||||
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache, noRevisionCache bool, localManifests []string, hasMultipleSources bool) (*comparisonResult, error) {
|
||||
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localManifests []string, hasMultipleSources bool) (*comparisonResult, error) {
|
||||
ts := stats.NewTimingStats()
|
||||
logCtx := log.WithFields(applog.GetAppLogFields(app))
|
||||
|
||||
|
||||
@@ -23,12 +23,37 @@ All following commands in this guide assume the namespace is already set.
|
||||
kubectl config set-context --current --namespace=argocd
|
||||
```
|
||||
|
||||
### Pull in all build dependencies
|
||||
### Pull in all UI build dependencies
|
||||
|
||||
As build dependencies change over time, you have to synchronize your development environment with the current specification. In order to pull in all required dependencies, issue:
|
||||
As build dependencies change over time, you have to synchronize your development environment with the current specification. In order to pull in all required UI dependencies (NPM packages), issue:
|
||||
|
||||
* `make dep-ui` or `make dep-ui-local`
|
||||
|
||||
These commands run `pnpm install --frozen-lockfile` command, which only brings package versions that are defined in the `pnpm-lock.yaml` file without trying to resolve and download new package versions.
|
||||
|
||||
### Updating UI build dependencies
|
||||
|
||||
If you need to add new UI dependencies or update existing ones you need
|
||||
to run a `pnpm` command in the ./ui directory to resolve and download new packages.
|
||||
|
||||
You can run it in the docker container using the `make run-pnpm` make target.
|
||||
|
||||
For example, to add new dependency `newpackage` you may run command like
|
||||
|
||||
```shell
|
||||
make run-pnpm PNPM_COMMAND="add newpackage --ignore-scripts"
|
||||
```
|
||||
|
||||
To upgrade an existing package:
|
||||
|
||||
```shell
|
||||
make run-pnpm PNPM_COMMAND="update existingpackage@1.0.2 --ignore-scripts"
|
||||
```
|
||||
|
||||
Please consider using best security practices when adding or upgrading
|
||||
NPM dependencies, such as this
|
||||
[guide](https://github.com/lirantal/npm-security-best-practices/blob/main/README.md).
|
||||
|
||||
### Generate API glue code and other assets
|
||||
|
||||
Argo CD relies on Google's [Protocol Buffers](https://developers.google.com/protocol-buffers) for its API, and this makes heavy use of auto-generated glue code and stubs. Whenever you touched parts of the API code, you must re-generate the auto generated code.
|
||||
|
||||
@@ -212,7 +212,7 @@ export IMAGE_TAG=1.5.0-myrc
|
||||
|
||||
> [!NOTE]
|
||||
> The image will be built for `linux/amd64` platform by default. If you are running on Mac with Apple chip (ARM),
|
||||
> you need to specify the correct buld platform by running:
|
||||
> you need to specify the correct build platform by running:
|
||||
> ```bash
|
||||
> export TARGET_ARCH=linux/arm64
|
||||
> ```
|
||||
|
||||
@@ -41,7 +41,7 @@ spec:
|
||||
- https://kubernetes.default.svc
|
||||
- https://some-other-cluster
|
||||
|
||||
# Git generator generates parametes either from directory structure of files within a git repo
|
||||
# Git generator generates parameters either from directory structure of files within a git repo
|
||||
- git:
|
||||
repoURL: https://github.com/argoproj/argo-cd.git
|
||||
# OPTIONAL: use directory structure of git repo to generate parameters
|
||||
|
||||
@@ -86,7 +86,7 @@ data:
|
||||
# Optional set of OIDC claims to request on the ID token.
|
||||
requestedIDTokenClaims: {"groups": {"essential": true}}
|
||||
|
||||
# Configuration to customize resource behavior (optional) can be configured via splitted sub keys.
|
||||
# Configuration to customize resource behavior (optional) can be configured via split sub keys.
|
||||
# Keys are in the form: resource.customizations.ignoreDifferences.<group_kind>, resource.customizations.health.<group_kind>
|
||||
# resource.customizations.actions.<group_kind>, resource.customizations.knownTypeFields.<group_kind>
|
||||
# resource.customizations.ignoreResourceUpdates.<group_kind>
|
||||
@@ -115,7 +115,7 @@ data:
|
||||
jsonPointers:
|
||||
- /metadata/resourceVersion
|
||||
|
||||
# Configuration to define customizations ignoring differences during watched resource updates can be configured via splitted sub key.
|
||||
# Configuration to define customizations ignoring differences during watched resource updates can be configured via split sub key.
|
||||
resource.customizations.ignoreResourceUpdates.argoproj.io_Application: |
|
||||
jsonPointers:
|
||||
- /status
|
||||
|
||||
@@ -438,6 +438,55 @@ spec:
|
||||
> paths
|
||||
> provided in the annotation. The application path serves as the deepest path that can be selected as the root.
|
||||
|
||||
#### Measuring Annotation Efficiency
|
||||
|
||||
You can use the following metrics to evaluate how effectively the `argocd.argoproj.io/manifest-generate-paths`
|
||||
annotation is reducing unnecessary manifest regeneration:
|
||||
|
||||
- **`argocd_webhook_requests_total`** (label: `repo`) — counts incoming webhook events per repository. Use this as the
|
||||
baseline for how many push events Argo CD is receiving.
|
||||
|
||||
- **`argocd_webhook_store_cache_attempts_total`** (labels: `repo`, `successful`) — counts attempts to reuse the previously
|
||||
cached manifests for the new commit SHA when an application's refresh paths have _not_ changed. A `successful=true`
|
||||
result means the cache was warmed for the new revision without re-generating manifests, which is the desired outcome.
|
||||
|
||||
To assess efficiency, compare the rate of `successful=true` attempts against the total webhook rate. A high ratio
|
||||
indicates the annotation is working well and preventing unnecessary manifest regeneration.
|
||||
|
||||
Note that some `successful=false` results are expected and not a cause for concern — they occur when Argo CD has not
|
||||
yet cached manifests for an application (e.g. after a restart or first sync), so there is nothing to carry forward to
|
||||
the new revision.
|
||||
|
||||
#### Disabling Manifest Cache Warming in Webhooks
|
||||
|
||||
In some cases, the manifest cache warming done by the webhook handler can hurt performance rather than help it:
|
||||
|
||||
- **Plain YAML repositories**: if applications use plain YAML manifests (no Helm or Kustomize rendering), manifest
|
||||
generation is fast and caching provides little benefit. Attempting to warm the cache for thousands of unaffected
|
||||
applications on every commit adds significant overhead.
|
||||
- **Large monorepos**: with many applications sharing a single repository, each webhook event triggers a cache warm
|
||||
attempt for every application whose paths did not change. With thousands of applications, this can cause the webhook
|
||||
handler to spend significant time on Redis operations, delaying the actual reconciliation trigger for the affected
|
||||
application.
|
||||
|
||||
When disabled, the webhook handler will only trigger reconciliation for applications whose files have changed and
|
||||
will skip all Redis cache operations for unaffected applications. This is the recommended setting for large monorepos
|
||||
with plain YAML manifests.
|
||||
|
||||
**Per-repository setting (recommended)**: set `webhookManifestCacheWarmDisabled: true` on the repository via the
|
||||
ArgoCD CLI or UI:
|
||||
|
||||
```bash
|
||||
argocd repo edit https://github.com/org/repo.git --webhook-manifest-cache-warm-disabled
|
||||
```
|
||||
|
||||
**Global setting**: to disable cache warming for all repositories, set the following environment variable on
|
||||
`argocd-server`:
|
||||
|
||||
```
|
||||
ARGOCD_WEBHOOK_MANIFEST_CACHE_WARM_DISABLED=true
|
||||
```
|
||||
|
||||
### Application Sync Timeout & Jitter
|
||||
|
||||
Argo CD has a timeout for application syncs. It will trigger a refresh for each application periodically when the
|
||||
|
||||
@@ -125,7 +125,7 @@ data:
|
||||
send: [on-deployed-template]
|
||||
```
|
||||
|
||||
Now, with the setup above, a sync will send the list of images to your Slack application. For more information about integratin with Slack, see the [Slack integration guide](services/slack.md).
|
||||
Now, with the setup above, a sync will send the list of images to your Slack application. For more information about integration with Slack, see the [Slack integration guide](services/slack.md).
|
||||
|
||||
### Deduplicating images
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ on how your workloads connect to the repository server.
|
||||
|
||||
### Configuring TLS to argocd-repo-server
|
||||
|
||||
The componenets `argocd-server`, `argocd-application-controller`, `argocd-notifications-controller`,
|
||||
The components `argocd-server`, `argocd-application-controller`, `argocd-notifications-controller`,
|
||||
and `argocd-applicationset-controller` communicate with the `argocd-repo-server`
|
||||
using a gRPC API over TLS. By default, `argocd-repo-server` generates a non-persistent,
|
||||
self-signed certificate to use for its gRPC endpoint on startup. Because the
|
||||
@@ -190,7 +190,7 @@ self-signed certificate to use for its gRPC endpoint on startup. Because the
|
||||
is not available to outside consumers for verification. These components will use a
|
||||
non-validating connection to the `argocd-repo-server` for this reason.
|
||||
|
||||
To change this behavior to be more secure by having these componenets validate the TLS certificate of the
|
||||
To change this behavior to be more secure by having these components validate the TLS certificate of the
|
||||
`argocd-repo-server` endpoint, the following steps need to be performed:
|
||||
|
||||
* Create a persistent TLS certificate to be used by `argocd-repo-server`, as
|
||||
|
||||
@@ -272,7 +272,7 @@ curl -X POST -H "Authorization: Bearer $ARGOCD_TOKEN" -H "Content-Type: applicat
|
||||
}' "http://$YOUR_ARGOCD_URL/api/v1/applications/$YOUR_APP_NAME/sync"
|
||||
```
|
||||
|
||||
It is also possible to sync such an Applicaton using the UI, with `ApplyOutOfSyncOnly` option unchecked. However, currently, performing a sync without `ApplyOutOfSyncOnly` option is not possible using the CLI.
|
||||
It is also possible to sync such an Application using the UI, with `ApplyOutOfSyncOnly` option unchecked. However, currently, performing a sync without `ApplyOutOfSyncOnly` option is not possible using the CLI.
|
||||
|
||||
##### Other users
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ When Argo CD is upgraded manually using plain manifests or Kustomize overlays, i
|
||||
Users upgrading Argo CD manually using `helm upgrade` are not impacted by this change, since Helm does not use client-side apply and does not result in creation of the `last-applied` annotation.
|
||||
|
||||
#### Users who previously upgraded to 3.3.0 or 3.3.1
|
||||
In some cases, after upgrading to one of those versions and applying Server-Side Apply, the following error occured:
|
||||
In some cases, after upgrading to one of those versions and applying Server-Side Apply, the following error occurred:
|
||||
`one or more synchronization tasks completed unsuccessfully, reason: Failed to perform client-side apply migration: failed to perform client-side apply migration on manager kubectl-client-side-apply: error when patching "/dev/shm/2047509016": CustomResourceDefinition.apiextensions.k8s.io "applicationsets.argoproj.io" is invalid: metadata.annotations: Too long: may not be more than 262144 bytes`.
|
||||
|
||||
Users that have configured the sync option `ClientSideApplyMigration=false` as a temporary remediation for the above error, should remove it after upgrading to `3.3.2`. Disabling `ClientSideApplyMigration` imposes a risk to encounter conflicts between K8s field managers in the future.
|
||||
|
||||
@@ -68,7 +68,7 @@ deploy:
|
||||
|
||||
## Configuring RBAC
|
||||
|
||||
When using ArgoCD global RBAC comfig map, you can define your `policy.csv` like so:
|
||||
When using ArgoCD global RBAC config map, you can define your `policy.csv` like so:
|
||||
|
||||
```yaml
|
||||
configs:
|
||||
|
||||
@@ -142,7 +142,7 @@ We provide the entire application tree to accomplish two things:
|
||||
Further, if an Extension needs richer information than that provided by the Resource Tree, it can request additional information about a resource from the Argo CD API server.
|
||||
|
||||
```typescript
|
||||
interface Extention {
|
||||
interface Extension {
|
||||
ResourceTab: React.Component<{resource: any}>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -135,7 +135,7 @@ one in charge of a given resource.
|
||||
|
||||
#### Include resource identifies in the `app.kubernetes.io/instance` annotation
|
||||
|
||||
The `app.kubernetes.io/instance` annotation might be accidently added or copied
|
||||
The `app.kubernetes.io/instance` annotation might be accidentally added or copied
|
||||
same as label. To prevent Argo CD confusion the annotation value should include
|
||||
the identifier of the resource annotation was applied to. The resource identifier
|
||||
includes the group, kind, namespace and name of the resource. It is proposed to use `;`
|
||||
|
||||
@@ -42,7 +42,7 @@ A bounty is a special proposal created under `docs/proposals/feature-bounties`.
|
||||
#### Claiming a Bounty
|
||||
* Argo will pay out bounties once a pull request implementing the requested features/changes/fixes is merged.
|
||||
* A bounty is limited to a single successful PR.
|
||||
* Those interested in working on the bounty are encouraged to comment on the issue, and users may team up to split a bounty if they prefer but collaboration is not required and users should not shame eachother for their preferences to work alone or together.
|
||||
* Those interested in working on the bounty are encouraged to comment on the issue, and users may team up to split a bounty if they prefer but collaboration is not required and users should not shame each other for their preferences to work alone or together.
|
||||
* A comment of interest does not constitute a claim and will not be treated as such.
|
||||
* The first pull request submitted that is ready for merge will be reviewed by maintainers. Maintainers will also consider any competing pull requests submitted within 24-hours. We expect this will be a very rare circumstance. If multiple, high-quality, merge ready pull requests are submitted, 3-5 Approvers for the sub-project will vote to decide the final pull request merged.
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ argocd admin repo generate-spec REPOURL [flags]
|
||||
--type string type of the repository, "git", "oci" or "helm" (default "git")
|
||||
--use-azure-workload-identity whether to use azure workload identity for authentication
|
||||
--username string username to the repository
|
||||
--webhook-manifest-cache-warm-disabled disable manifest cache warming during webhook processing for this repository (recommended for large monorepos with plain YAML manifests)
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
1
docs/user-guide/commands/argocd_repo_add.md
generated
1
docs/user-guide/commands/argocd_repo_add.md
generated
@@ -87,6 +87,7 @@ argocd repo add REPOURL [flags]
|
||||
--upsert Override an existing repository with the same name even if the spec differs
|
||||
--use-azure-workload-identity whether to use azure workload identity for authentication
|
||||
--username string username to the repository
|
||||
--webhook-manifest-cache-warm-disabled disable manifest cache warming during webhook processing for this repository (recommended for large monorepos with plain YAML manifests)
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
4
go.mod
4
go.mod
@@ -7,7 +7,7 @@ require (
|
||||
dario.cat/mergo v1.0.2
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
|
||||
github.com/Azure/kubelogin v0.2.16
|
||||
github.com/Azure/kubelogin v0.2.17
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/Masterminds/sprig/v3 v3.3.0
|
||||
github.com/TomOnTime/utfutil v1.0.0
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/chainguard-dev/git-urls v1.0.2
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/coreos/go-oidc/v3 v3.18.0
|
||||
github.com/cyphar/filepath-securejoin v0.6.1
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
|
||||
8
go.sum
8
go.sum
@@ -72,8 +72,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
|
||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/Azure/kubelogin v0.2.16 h1:z0jwNQ9A7LvIqS0Go+6CPZv0TuQQRL2mc+zY9wjBuF8=
|
||||
github.com/Azure/kubelogin v0.2.16/go.mod h1:UvizZ5Gu/2btUFXm2cccbxliK/ensgBD5NTCihZoONE=
|
||||
github.com/Azure/kubelogin v0.2.17 h1:pRM+KHVo5Oj3aBUDbcrUTxdZHOPs02D3oZn2E3t1B4A=
|
||||
github.com/Azure/kubelogin v0.2.17/go.mod h1:UcOYtp0xCIn6tg0Fl3m0WOQDKcQA8Fb22Ya/b/DDaf0=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
@@ -217,8 +217,8 @@ github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJ
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
|
||||
github.com/codeskyblue/go-sh v0.0.0-20190412065543-76bd3d59ff27/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
|
||||
github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
|
||||
1602
pkg/apis/application/v1alpha1/generated.pb.go
generated
1602
pkg/apis/application/v1alpha1/generated.pb.go
generated
File diff suppressed because it is too large
Load Diff
@@ -1968,6 +1968,11 @@ message Repository {
|
||||
|
||||
// Depth specifies the depth for shallow clones. A value of 0 or omitting the field indicates a full clone.
|
||||
optional int64 depth = 27;
|
||||
|
||||
// WebhookManifestCacheWarmDisabled disables manifest cache warming during webhook processing for this repository.
|
||||
// When set, webhook handlers will only trigger reconciliation for affected applications and skip Redis cache
|
||||
// operations for unaffected ones. Recommended for large monorepos with plain YAML manifests.
|
||||
optional bool webhookManifestCacheWarmDisabled = 28;
|
||||
}
|
||||
|
||||
// A RepositoryCertificate is either SSH known hosts entry or TLS certificate
|
||||
|
||||
@@ -116,6 +116,10 @@ type Repository struct {
|
||||
InsecureOCIForceHttp bool `json:"insecureOCIForceHttp,omitempty" protobuf:"bytes,26,opt,name=insecureOCIForceHttp"` //nolint:revive //FIXME(var-naming)
|
||||
// Depth specifies the depth for shallow clones. A value of 0 or omitting the field indicates a full clone.
|
||||
Depth int64 `json:"depth,omitempty" protobuf:"bytes,27,opt,name=depth"`
|
||||
// WebhookManifestCacheWarmDisabled disables manifest cache warming during webhook processing for this repository.
|
||||
// When set, webhook handlers will only trigger reconciliation for affected applications and skip Redis cache
|
||||
// operations for unaffected ones. Recommended for large monorepos with plain YAML manifests.
|
||||
WebhookManifestCacheWarmDisabled bool `json:"webhookManifestCacheWarmDisabled,omitempty" protobuf:"varint,28,opt,name=webhookManifestCacheWarmDisabled"`
|
||||
}
|
||||
|
||||
// IsInsecure returns true if the repository has been configured to skip server verification or set to HTTP only
|
||||
|
||||
@@ -1908,17 +1908,23 @@ func (s *Server) PodLogs(q *application.ApplicationPodLogsQuery, ws application.
|
||||
// if k8s failed to start steaming logs (typically because Pod is not ready yet)
|
||||
// then the error should be shown in the UI so that user know the reason
|
||||
if err != nil {
|
||||
logStream <- logEntry{line: err.Error()}
|
||||
select {
|
||||
case logStream <- logEntry{line: err.Error()}:
|
||||
case <-ws.Context().Done():
|
||||
}
|
||||
} else {
|
||||
parseLogsStream(podName, stream, logStream)
|
||||
parseLogsStream(ws.Context(), podName, stream, logStream)
|
||||
}
|
||||
close(logStream)
|
||||
}()
|
||||
}
|
||||
|
||||
logStream := mergeLogStreams(streams, time.Millisecond*100)
|
||||
logStream := mergeLogStreams(ws.Context(), streams, time.Millisecond*100)
|
||||
sentCount := int64(0)
|
||||
done := make(chan error)
|
||||
// Buffered so the goroutine below can always send and exit, even if PodLogs has already
|
||||
// returned due to client disconnect (ws.Context().Done). Without this, the goroutine
|
||||
// would block on "done <- err" forever, leaking memory via bufio and mergeLogStreams buffers.
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
for entry := range logStream {
|
||||
if entry.err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package application
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
@@ -17,8 +18,9 @@ type logEntry struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// parseLogsStream converts given ReadCloser into channel that emits log entries
|
||||
func parseLogsStream(podName string, stream io.ReadCloser, ch chan logEntry) {
|
||||
// parseLogsStream converts given ReadCloser into channel that emits log entries.
|
||||
// It stops early if ctx is cancelled, avoiding goroutine leaks when the caller disconnects.
|
||||
func parseLogsStream(ctx context.Context, podName string, stream io.ReadCloser, ch chan logEntry) {
|
||||
bufReader := bufio.NewReader(stream)
|
||||
eof := false
|
||||
for !eof {
|
||||
@@ -30,7 +32,10 @@ func parseLogsStream(podName string, stream io.ReadCloser, ch chan logEntry) {
|
||||
break
|
||||
}
|
||||
} else if err != nil && !errors.Is(err, io.EOF) {
|
||||
ch <- logEntry{err: err}
|
||||
select {
|
||||
case ch <- logEntry{err: err}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -39,13 +44,20 @@ func parseLogsStream(podName string, stream io.ReadCloser, ch chan logEntry) {
|
||||
timeStampStr := parts[0]
|
||||
logTime, err := time.Parse(time.RFC3339Nano, timeStampStr)
|
||||
if err != nil {
|
||||
ch <- logEntry{err: err}
|
||||
select {
|
||||
case ch <- logEntry{err: err}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
lines := strings.Join(parts[1:], " ")
|
||||
for line := range strings.SplitSeq(lines, "\r") {
|
||||
ch <- logEntry{line: line, timeStamp: logTime, podName: podName}
|
||||
select {
|
||||
case ch <- logEntry{line: line, timeStamp: logTime, podName: podName}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +65,8 @@ func parseLogsStream(podName string, stream io.ReadCloser, ch chan logEntry) {
|
||||
// mergeLogStreams merge two stream of logs and ensures that merged logs are sorted by timestamp.
|
||||
// The implementation uses merge sort: method reads next log entry from each stream if one of streams is empty
|
||||
// it waits for no longer than specified duration and then merges available entries.
|
||||
func mergeLogStreams(streams []chan logEntry, bufferingDuration time.Duration) chan logEntry {
|
||||
// ctx cancellation causes all internal goroutines to exit promptly, preventing goroutine and memory leaks.
|
||||
func mergeLogStreams(ctx context.Context, streams []chan logEntry, bufferingDuration time.Duration) chan logEntry {
|
||||
merged := make(chan logEntry)
|
||||
|
||||
// buffer of received log entries for each stream
|
||||
@@ -70,7 +83,17 @@ func mergeLogStreams(streams []chan logEntry, bufferingDuration time.Duration) c
|
||||
lock.Lock()
|
||||
entriesPerStream[index] = append(entriesPerStream[index], next)
|
||||
lock.Unlock()
|
||||
process <- struct{}{}
|
||||
select {
|
||||
case process <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
// drain remaining entries so parseLogsStream goroutine can exit
|
||||
for range streams[index] {
|
||||
}
|
||||
if atomic.AddInt32(&streamsCount, -1) == 0 {
|
||||
close(process)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
// stop processing after all streams got closed
|
||||
if atomic.AddInt32(&streamsCount, -1) == 0 {
|
||||
@@ -111,7 +134,11 @@ func mergeLogStreams(streams []chan logEntry, bufferingDuration time.Duration) c
|
||||
}
|
||||
lock.Unlock()
|
||||
for i := range entries {
|
||||
merged <- entries[i]
|
||||
select {
|
||||
case merged <- entries[i]:
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(entries) > 0
|
||||
}
|
||||
@@ -120,11 +147,11 @@ func mergeLogStreams(streams []chan logEntry, bufferingDuration time.Duration) c
|
||||
var sentAt time.Time
|
||||
|
||||
ticker := time.NewTicker(bufferingDuration)
|
||||
done := make(chan struct{})
|
||||
tickerDone := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
case <-tickerDone:
|
||||
return
|
||||
case <-ticker.C:
|
||||
sentAtLock.Lock()
|
||||
@@ -133,18 +160,30 @@ func mergeLogStreams(streams []chan logEntry, bufferingDuration time.Duration) c
|
||||
_ = send(true)
|
||||
sentAt = time.Now()
|
||||
}
|
||||
|
||||
sentAtLock.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for range process {
|
||||
if send(false) {
|
||||
sentAtLock.Lock()
|
||||
sentAt = time.Now()
|
||||
sentAtLock.Unlock()
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-process:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
if send(false) {
|
||||
sentAtLock.Lock()
|
||||
sentAt = time.Now()
|
||||
sentAtLock.Unlock()
|
||||
}
|
||||
case <-ctx.Done():
|
||||
// client disconnected: stop immediately without flushing
|
||||
ticker.Stop()
|
||||
tickerDone <- struct{}{}
|
||||
close(merged)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,10 +191,10 @@ func mergeLogStreams(streams []chan logEntry, bufferingDuration time.Duration) c
|
||||
|
||||
ticker.Stop()
|
||||
// ticker.Stop() does not close the channel, and it does not wait for the channel to be drained. So we need to
|
||||
// explicitly prevent the gorountine from leaking by closing the channel. We also need to prevent the goroutine
|
||||
// explicitly prevent the goroutine from leaking by closing the channel. We also need to prevent the goroutine
|
||||
// from calling `send` again, because `send` pushes to the `merged` channel which we're about to close.
|
||||
// This describes the approach nicely: https://stackoverflow.com/questions/17797754/ticker-stop-behaviour-in-golang
|
||||
done <- struct{}{}
|
||||
tickerDone <- struct{}{}
|
||||
close(merged)
|
||||
}()
|
||||
return merged
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -16,7 +17,7 @@ func TestParseLogsStream_Successful(t *testing.T) {
|
||||
|
||||
res := make(chan logEntry)
|
||||
go func() {
|
||||
parseLogsStream("test", r, res)
|
||||
parseLogsStream(context.Background(), "test", r, res)
|
||||
close(res)
|
||||
}()
|
||||
|
||||
@@ -39,7 +40,7 @@ func TestParseLogsStream_ParsingError(t *testing.T) {
|
||||
|
||||
res := make(chan logEntry)
|
||||
go func() {
|
||||
parseLogsStream("test", r, res)
|
||||
parseLogsStream(context.Background(), "test", r, res)
|
||||
close(res)
|
||||
}()
|
||||
|
||||
@@ -55,19 +56,19 @@ func TestParseLogsStream_ParsingError(t *testing.T) {
|
||||
func TestMergeLogStreams(t *testing.T) {
|
||||
first := make(chan logEntry)
|
||||
go func() {
|
||||
parseLogsStream("first", io.NopCloser(strings.NewReader(`2021-02-09T00:00:01Z 1
|
||||
parseLogsStream(context.Background(), "first", io.NopCloser(strings.NewReader(`2021-02-09T00:00:01Z 1
|
||||
2021-02-09T00:00:03Z 3`)), first)
|
||||
close(first)
|
||||
}()
|
||||
|
||||
second := make(chan logEntry)
|
||||
go func() {
|
||||
parseLogsStream("second", io.NopCloser(strings.NewReader(`2021-02-09T00:00:02Z 2
|
||||
parseLogsStream(context.Background(), "second", io.NopCloser(strings.NewReader(`2021-02-09T00:00:02Z 2
|
||||
2021-02-09T00:00:04Z 4`)), second)
|
||||
close(second)
|
||||
}()
|
||||
|
||||
merged := mergeLogStreams([]chan logEntry{first, second}, time.Second)
|
||||
merged := mergeLogStreams(context.Background(), []chan logEntry{first, second}, time.Second)
|
||||
var lines []string
|
||||
for entry := range merged {
|
||||
lines = append(lines, entry.line)
|
||||
@@ -83,18 +84,18 @@ func TestMergeLogStreams_RaceCondition(_ *testing.T) {
|
||||
second := make(chan logEntry)
|
||||
|
||||
go func() {
|
||||
parseLogsStream("first", io.NopCloser(strings.NewReader(`2021-02-09T00:00:01Z 1`)), first)
|
||||
parseLogsStream(context.Background(), "first", io.NopCloser(strings.NewReader(`2021-02-09T00:00:01Z 1`)), first)
|
||||
time.Sleep(time.Duration(i%3) * time.Millisecond)
|
||||
close(first)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
parseLogsStream("second", io.NopCloser(strings.NewReader(`2021-02-09T00:00:02Z 2`)), second)
|
||||
parseLogsStream(context.Background(), "second", io.NopCloser(strings.NewReader(`2021-02-09T00:00:02Z 2`)), second)
|
||||
time.Sleep(time.Duration((i+1)%3) * time.Millisecond)
|
||||
close(second)
|
||||
}()
|
||||
|
||||
merged := mergeLogStreams([]chan logEntry{first, second}, 1*time.Millisecond)
|
||||
merged := mergeLogStreams(context.Background(), []chan logEntry{first, second}, 1*time.Millisecond)
|
||||
|
||||
// Drain the channel
|
||||
for range merged {
|
||||
@@ -105,3 +106,39 @@ func TestMergeLogStreams_RaceCondition(_ *testing.T) {
|
||||
// and channel closer.
|
||||
}
|
||||
}
|
||||
|
||||
// TestMergeLogStreams_ContextCancellation verifies that cancelling the context causes mergeLogStreams
|
||||
// to close the merged channel promptly, allowing all internal goroutines to exit without leaking.
|
||||
func TestMergeLogStreams_ContextCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// unbuffered pipe: write end will block until someone reads
|
||||
pr, pw := io.Pipe()
|
||||
|
||||
ch := make(chan logEntry)
|
||||
go func() {
|
||||
parseLogsStream(ctx, "test", pr, ch)
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
merged := mergeLogStreams(ctx, []chan logEntry{ch}, time.Second)
|
||||
|
||||
// cancel before the pipe produces any data
|
||||
cancel()
|
||||
_ = pw.Close()
|
||||
|
||||
// merged must be closed (context cancelled), not block forever
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for range merged {
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// merged closed promptly — no leak
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("mergeLogStreams did not close merged channel after context cancellation")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ RUN ln -s /usr/lib/$(uname -m)-linux-gnu /usr/lib/linux-gnu
|
||||
# Please make sure to also check the contained yarn version and update the references below when upgrading this image's version
|
||||
FROM docker.io/library/node:22.9.0@sha256:8398ea18b8b72817c84af283f72daed9629af2958c4f618fe6db4f453c5c9328 AS node
|
||||
|
||||
FROM docker.io/library/golang:1.26.1@sha256:cd78d88e00afadbedd272f977d375a6247455f3a4b1178f8ae8bbcb201743a8a AS golang
|
||||
FROM docker.io/library/golang:1.26.2@sha256:2a2b4b5791cea8ae09caecba7bad0bd9631def96e5fe362e4a5e67009fe4ae61 AS golang
|
||||
|
||||
FROM docker.io/library/registry:3.1@sha256:afcd13fd045b8859ac4f60fef26fc2d2f9b7b9d9e604c3c4f7c2fb1b94f95a64 AS registry
|
||||
|
||||
|
||||
@@ -128,6 +128,9 @@ func NewFakeSecret() *corev1.Secret {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDSecretName,
|
||||
Namespace: FakeArgoCDNamespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"admin.password": []byte("test"),
|
||||
|
||||
@@ -19,7 +19,9 @@ RUN dpkg-divert --add --rename --divert /opt/google/chrome/google-chrome.real /o
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY package*.json ./
|
||||
|
||||
COPY pnpm-lock.yaml ./
|
||||
RUN npm install -g corepack@0.34.6 && corepack enable && pnpm install
|
||||
RUN npm install -g corepack@0.34.6 && corepack enable && pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Web UI for [Argo CD](https://github.com/argoproj/argo-cd).
|
||||
## Getting started
|
||||
|
||||
1. Install [NodeJS](https://nodejs.org/en/download/) and [pnpm](https://pnpm.io). On macOS with [Homebrew](https://brew.sh/), running `brew install node pnpm` will accomplish this.
|
||||
2. Run `pnpm install` to install local prerequisites.
|
||||
2. Run `pnpm install --frozen-lockfile` to install local prerequisites.
|
||||
3. Run `pnpm start` to launch the webpack dev UI server.
|
||||
4. Run `pnpm build` to bundle static resources into the `./dist` directory.
|
||||
|
||||
|
||||
@@ -33,6 +33,6 @@ export default [
|
||||
files: ['./src/**/*.{ts,tsx}']
|
||||
},
|
||||
{
|
||||
ignores: ['dist', 'assets', '**/*.config.js', '__mocks__', 'coverage', '**/*.test.{ts,tsx}']
|
||||
ignores: ['dist', 'assets', '**/*.config.js', 'jest.setup.js', '__mocks__', 'coverage', '**/*.test.{ts,tsx}']
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
setupFiles: ['./jest.setup.js'],
|
||||
reporters: ['default', 'jest-junit'],
|
||||
collectCoverage: true,
|
||||
transformIgnorePatterns: ['node_modules/(?!(argo-ui|.*\\.pnpm.*argo-ui.*)/)'],
|
||||
|
||||
9
ui/jest.setup.js
Normal file
9
ui/jest.setup.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// TODO: This needs to be polyfilled until jest-environment-jsdom decides to pull in a version of jsdom that's >=27.4.0
|
||||
const {TextEncoder, TextDecoder} = require('util');
|
||||
|
||||
if (typeof globalThis.TextEncoder === 'undefined') {
|
||||
globalThis.TextEncoder = TextEncoder;
|
||||
}
|
||||
if (typeof globalThis.TextDecoder === 'undefined') {
|
||||
globalThis.TextDecoder = TextDecoder;
|
||||
}
|
||||
@@ -64,7 +64,7 @@
|
||||
"@types/react-dom": "^16.8.2",
|
||||
"normalize-url": "4.5.1",
|
||||
"rxjs": "6.6.7",
|
||||
"formidable": "2.1.2"
|
||||
"formidable": "2.1.3"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -76,7 +76,7 @@
|
||||
"@eslint/js": "^9.1.1",
|
||||
"@types/classnames": "^2.2.3",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/dagre": "0.7.42",
|
||||
"@types/dagre": "0.7.54",
|
||||
"@types/deepmerge": "^2.2.0",
|
||||
"@types/git-url-parse": "^9.0.1",
|
||||
"@types/history": "^4.7.2",
|
||||
|
||||
56
ui/pnpm-lock.yaml
generated
56
ui/pnpm-lock.yaml
generated
@@ -9,7 +9,7 @@ overrides:
|
||||
'@types/react-dom': ^16.8.2
|
||||
normalize-url: 4.5.1
|
||||
rxjs: 6.6.7
|
||||
formidable: 2.1.2
|
||||
formidable: 2.1.3
|
||||
|
||||
importers:
|
||||
|
||||
@@ -29,7 +29,7 @@ importers:
|
||||
version: 6.1.6(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
|
||||
argo-ui:
|
||||
specifier: git+https://github.com/argoproj/argo-ui.git
|
||||
version: https://codeload.github.com/argoproj/argo-ui/tar.gz/2bfda77cec418c4123fe61e35f22d09432af15b7(@types/react@16.14.65)(jquery@3.7.1)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(what-input@5.2.12)
|
||||
version: https://codeload.github.com/argoproj/argo-ui/tar.gz/a1c32a45e83fdda4baafc7ca3105c3ead383f8ba(@types/react@16.14.65)(jquery@3.7.1)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(what-input@5.2.12)
|
||||
buffer:
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3
|
||||
@@ -176,8 +176,8 @@ importers:
|
||||
specifier: ^0.5.1
|
||||
version: 0.5.4
|
||||
'@types/dagre':
|
||||
specifier: 0.7.42
|
||||
version: 0.7.42
|
||||
specifier: 0.7.54
|
||||
version: 0.7.54
|
||||
'@types/deepmerge':
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.3
|
||||
@@ -1455,6 +1455,10 @@ packages:
|
||||
'@leichtgewicht/ip-codec@2.0.5':
|
||||
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
|
||||
|
||||
'@noble/hashes@1.8.0':
|
||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1475,6 +1479,9 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
deprecated: This functionality has been moved to @npmcli/fs
|
||||
|
||||
'@paralleldrive/cuid2@2.3.1':
|
||||
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -1641,8 +1648,8 @@ packages:
|
||||
'@types/cookiejar@2.1.5':
|
||||
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
||||
|
||||
'@types/dagre@0.7.42':
|
||||
resolution: {integrity: sha512-knVdi1Ul8xYgJ0wdhQ+/2YGJFKJFa/5srcPII9zvOs4KhsHfpnFrSTQXATYmjslglxRMif3Lg+wEZ0beag+94A==}
|
||||
'@types/dagre@0.7.54':
|
||||
resolution: {integrity: sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==}
|
||||
|
||||
'@types/deepmerge@2.2.3':
|
||||
resolution: {integrity: sha512-ct4srnukH/SHdVPyJIFV73YJIt9PTYTaqQbjrCvRrbc9LxHdGcJb132SuWwnDTPyx5UjCVS/I00wj0i5IXfqSA==}
|
||||
@@ -2078,8 +2085,8 @@ packages:
|
||||
arg@4.1.3:
|
||||
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||
|
||||
argo-ui@https://codeload.github.com/argoproj/argo-ui/tar.gz/2bfda77cec418c4123fe61e35f22d09432af15b7:
|
||||
resolution: {tarball: https://codeload.github.com/argoproj/argo-ui/tar.gz/2bfda77cec418c4123fe61e35f22d09432af15b7}
|
||||
argo-ui@https://codeload.github.com/argoproj/argo-ui/tar.gz/a1c32a45e83fdda4baafc7ca3105c3ead383f8ba:
|
||||
resolution: {tarball: https://codeload.github.com/argoproj/argo-ui/tar.gz/a1c32a45e83fdda4baafc7ca3105c3ead383f8ba}
|
||||
version: 1.0.0
|
||||
peerDependencies:
|
||||
'@types/react': ^16.9.3
|
||||
@@ -2596,6 +2603,7 @@ packages:
|
||||
|
||||
deep-diff@0.3.8:
|
||||
resolution: {integrity: sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug==}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
|
||||
deep-equal@1.1.2:
|
||||
resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==}
|
||||
@@ -3073,9 +3081,9 @@ packages:
|
||||
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
formidable@2.1.2:
|
||||
resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==}
|
||||
deprecated: 'ACTION REQUIRED: SWITCH TO v3 - v1 and v2 are VULNERABLE! v1 is DEPRECATED FOR OVER 2 YEARS! Use formidable@latest or try formidable-mini for fresh projects'
|
||||
formidable@2.1.3:
|
||||
resolution: {integrity: sha512-vDI5JjeALeGXpyL8v71ZG2VgHY5zD6qg1IvypU7aJCYvREZyhawrYJxMdsWO+m5DIGLiMiDH71yEN8RO4wQAMQ==}
|
||||
deprecated: 'ATTENTION: please upgrade to v3! The v1 and v2 versions are pretty old and deprecated'
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
@@ -3168,7 +3176,7 @@ packages:
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
global@4.4.0:
|
||||
resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
|
||||
@@ -3243,10 +3251,6 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
hexoid@1.0.0:
|
||||
resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
history@4.10.1:
|
||||
resolution: {integrity: sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==}
|
||||
|
||||
@@ -5132,6 +5136,7 @@ packages:
|
||||
tar@6.2.1:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
teeny-request@7.1.1:
|
||||
resolution: {integrity: sha512-iwY6rkW5DDGq8hE2YgNQlKbptYpY5Nn2xecjQiNjOXWbKzPGUfmeUBCSQbbr306d7Z7U2N0TPl+/SwYRfua1Dg==}
|
||||
@@ -5526,6 +5531,7 @@ packages:
|
||||
whatwg-encoding@2.0.0:
|
||||
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
||||
|
||||
whatwg-mimetype@3.0.0:
|
||||
resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
|
||||
@@ -6889,6 +6895,8 @@ snapshots:
|
||||
|
||||
'@leichtgewicht/ip-codec@2.0.5': {}
|
||||
|
||||
'@noble/hashes@1.8.0': {}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -6911,6 +6919,10 @@ snapshots:
|
||||
mkdirp: 1.0.4
|
||||
rimraf: 3.0.2
|
||||
|
||||
'@paralleldrive/cuid2@2.3.1':
|
||||
dependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
optional: true
|
||||
|
||||
@@ -7073,7 +7085,7 @@ snapshots:
|
||||
|
||||
'@types/cookiejar@2.1.5': {}
|
||||
|
||||
'@types/dagre@0.7.42': {}
|
||||
'@types/dagre@0.7.54': {}
|
||||
|
||||
'@types/deepmerge@2.2.3':
|
||||
dependencies:
|
||||
@@ -7581,7 +7593,7 @@ snapshots:
|
||||
|
||||
arg@4.1.3: {}
|
||||
|
||||
argo-ui@https://codeload.github.com/argoproj/argo-ui/tar.gz/2bfda77cec418c4123fe61e35f22d09432af15b7(@types/react@16.14.65)(jquery@3.7.1)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(what-input@5.2.12):
|
||||
argo-ui@https://codeload.github.com/argoproj/argo-ui/tar.gz/a1c32a45e83fdda4baafc7ca3105c3ead383f8ba(@types/react@16.14.65)(jquery@3.7.1)(react-dom@16.14.0(react@16.14.0))(react@16.14.0)(what-input@5.2.12):
|
||||
dependencies:
|
||||
'@fortawesome/fontawesome-free': 6.7.2
|
||||
'@tippy.js/react': 3.1.1(react-dom@16.14.0(react@16.14.0))(react@16.14.0)
|
||||
@@ -8846,10 +8858,10 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
formidable@2.1.2:
|
||||
formidable@2.1.3:
|
||||
dependencies:
|
||||
'@paralleldrive/cuid2': 2.3.1
|
||||
dezalgo: 1.0.4
|
||||
hexoid: 1.0.0
|
||||
once: 1.4.0
|
||||
qs: 6.15.0
|
||||
|
||||
@@ -9013,8 +9025,6 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
hexoid@1.0.0: {}
|
||||
|
||||
history@4.10.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.6
|
||||
@@ -11224,7 +11234,7 @@ snapshots:
|
||||
debug: 4.4.1
|
||||
fast-safe-stringify: 2.1.1
|
||||
form-data: 4.0.4
|
||||
formidable: 2.1.2
|
||||
formidable: 2.1.3
|
||||
methods: 1.1.2
|
||||
mime: 2.6.0
|
||||
qs: 6.15.0
|
||||
|
||||
@@ -112,7 +112,7 @@ function getGraphSize(nodes: dagre.Node[]): {width: number; height: number} {
|
||||
return {width, height};
|
||||
}
|
||||
|
||||
function groupNodes(nodes: ResourceTreeNode[], graph: dagre.graphlib.Graph) {
|
||||
function groupNodes(nodes: ResourceTreeNode[], graph: dagre.graphlib.Graph<{[key: string]: any}>) {
|
||||
function getNodeGroupingInfo(nodeId: string) {
|
||||
const node = graph.node(nodeId);
|
||||
return {
|
||||
@@ -280,7 +280,7 @@ function renderFilteredNode(node: {count: number} & dagre.Node, onClearFilter: (
|
||||
);
|
||||
}
|
||||
|
||||
function renderGroupedNodes(props: ApplicationResourceTreeProps, node: {count: number} & dagre.Node & ResourceTreeNode) {
|
||||
function renderGroupedNodes(props: ApplicationResourceTreeProps, node: {count: number; groupedNodeIds: string[]} & dagre.Node & ResourceTreeNode) {
|
||||
const indicators = new Array<number>();
|
||||
let count = Math.min(node.count - 1, 3);
|
||||
while (count > 0) {
|
||||
@@ -333,7 +333,7 @@ function renderTrafficNode(node: dagre.Node) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderLoadBalancerNode(node: dagre.Node & {label: string; color: string}) {
|
||||
function renderLoadBalancerNode(node: dagre.Node & {label: string; color: string; kind: string}) {
|
||||
return (
|
||||
<div
|
||||
className='application-resource-tree__node application-resource-tree__node--load-balancer'
|
||||
@@ -400,7 +400,12 @@ function processPodGroup(targetPodGroup: ResourceTreeNode, child: ResourceTreeNo
|
||||
}
|
||||
}
|
||||
|
||||
function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: ResourceTreeNode & dagre.Node, childMap: Map<string, ResourceTreeNode[]>) {
|
||||
function renderPodGroup(
|
||||
props: ApplicationResourceTreeProps,
|
||||
id: string,
|
||||
node: ResourceTreeNode & dagre.Node & {groupedNodeIds?: string[]},
|
||||
childMap: Map<string, ResourceTreeNode[]>
|
||||
) {
|
||||
const fullName = nodeKey(node);
|
||||
let comparisonStatus: models.SyncStatusCode = null;
|
||||
let healthState: models.HealthStatus = null;
|
||||
@@ -937,7 +942,7 @@ function findNetworkTargets(nodes: ResourceTreeNode[], networkingInfo: models.Re
|
||||
return result;
|
||||
}
|
||||
export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) => {
|
||||
const graph = new dagre.graphlib.Graph();
|
||||
const graph = new dagre.graphlib.Graph<{[key: string]: any}>();
|
||||
graph.setGraph({nodesep: 25, rankdir: 'LR', marginy: 45, marginx: -100, ranksep: 80});
|
||||
graph.setDefaultEdgeLabel(() => ({}));
|
||||
const overridesCount = getAppOverridesCount(props.app);
|
||||
@@ -1025,7 +1030,12 @@ export const ApplicationResourceTree = (props: ApplicationResourceTreeProps) =>
|
||||
}
|
||||
}, [podCount]);
|
||||
|
||||
function filterGraph(app: models.AbstractApplication, filteredIndicatorParent: string, graphNodesFilter: dagre.graphlib.Graph, predicate: (node: ResourceTreeNode) => boolean) {
|
||||
function filterGraph(
|
||||
app: models.AbstractApplication,
|
||||
filteredIndicatorParent: string,
|
||||
graphNodesFilter: dagre.graphlib.Graph<{[key: string]: any}>,
|
||||
predicate: (node: ResourceTreeNode) => boolean
|
||||
) {
|
||||
const appKey = appNodeKey(app);
|
||||
let filtered = 0;
|
||||
graphNodesFilter.nodes().forEach(nodeId => {
|
||||
|
||||
@@ -407,6 +407,12 @@ func secretToRepository(secret *corev1.Secret) (*appsv1.Repository, error) {
|
||||
}
|
||||
repository.Depth = depth
|
||||
|
||||
webhookManifestCacheWarmDisabled, err := boolOrFalse(secret, "webhookManifestCacheWarmDisabled")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.WebhookManifestCacheWarmDisabled = webhookManifestCacheWarmDisabled
|
||||
|
||||
return repository, nil
|
||||
}
|
||||
|
||||
@@ -444,6 +450,7 @@ func (s *secretsRepositoryBackend) repositoryToSecret(repository *appsv1.Reposit
|
||||
updateSecretBool(secretCopy, "forceHttpBasicAuth", repository.ForceHttpBasicAuth)
|
||||
updateSecretBool(secretCopy, "useAzureWorkloadIdentity", repository.UseAzureWorkloadIdentity)
|
||||
updateSecretInt(secretCopy, "depth", repository.Depth)
|
||||
updateSecretBool(secretCopy, "webhookManifestCacheWarmDisabled", repository.WebhookManifestCacheWarmDisabled)
|
||||
addSecretMetadata(secretCopy, s.getSecretType())
|
||||
|
||||
return secretCopy
|
||||
|
||||
@@ -1205,6 +1205,71 @@ func TestCreateReadAndWriteSecretForSameURL(t *testing.T) {
|
||||
assert.Equal(t, common.LabelValueSecretTypeRepositoryWrite, writeSecret.Labels[common.LabelKeySecretType])
|
||||
}
|
||||
|
||||
func TestRepositoryToSecret(t *testing.T) {
|
||||
clientset := getClientset()
|
||||
testee := &secretsRepositoryBackend{db: &db{
|
||||
ns: testNamespace,
|
||||
kubeclientset: clientset,
|
||||
settingsMgr: settings.NewSettingsManager(t.Context(), clientset, testNamespace),
|
||||
}}
|
||||
s := &corev1.Secret{}
|
||||
repo := &appsv1.Repository{
|
||||
Name: "Name",
|
||||
Repo: "git@github.com:argoproj/argo-cd.git",
|
||||
Username: "Username",
|
||||
Password: "Password",
|
||||
SSHPrivateKey: "SSHPrivateKey",
|
||||
InsecureIgnoreHostKey: true,
|
||||
Insecure: true,
|
||||
EnableLFS: true,
|
||||
EnableOCI: true,
|
||||
InsecureOCIForceHttp: true,
|
||||
TLSClientCertData: "TLSClientCertData",
|
||||
TLSClientCertKey: "TLSClientCertKey",
|
||||
Type: "Type",
|
||||
GithubAppPrivateKey: "GithubAppPrivateKey",
|
||||
GithubAppId: 123,
|
||||
GithubAppInstallationId: 456,
|
||||
GitHubAppEnterpriseBaseURL: "GitHubAppEnterpriseBaseURL",
|
||||
Proxy: "Proxy",
|
||||
NoProxy: "NoProxy",
|
||||
Project: "Project",
|
||||
GCPServiceAccountKey: "GCPServiceAccountKey",
|
||||
ForceHttpBasicAuth: true,
|
||||
UseAzureWorkloadIdentity: true,
|
||||
Depth: 1,
|
||||
WebhookManifestCacheWarmDisabled: true,
|
||||
}
|
||||
s = testee.repositoryToSecret(repo, s)
|
||||
assert.Equal(t, []byte(repo.Name), s.Data["name"])
|
||||
assert.Equal(t, []byte(repo.Repo), s.Data["url"])
|
||||
assert.Equal(t, []byte(repo.Username), s.Data["username"])
|
||||
assert.Equal(t, []byte(repo.Password), s.Data["password"])
|
||||
assert.Equal(t, []byte(repo.SSHPrivateKey), s.Data["sshPrivateKey"])
|
||||
assert.Equal(t, []byte(strconv.FormatBool(repo.InsecureIgnoreHostKey)), s.Data["insecureIgnoreHostKey"])
|
||||
assert.Equal(t, []byte(strconv.FormatBool(repo.Insecure)), s.Data["insecure"])
|
||||
assert.Equal(t, []byte(strconv.FormatBool(repo.EnableLFS)), s.Data["enableLfs"])
|
||||
assert.Equal(t, []byte(strconv.FormatBool(repo.EnableOCI)), s.Data["enableOCI"])
|
||||
assert.Equal(t, []byte(strconv.FormatBool(repo.InsecureOCIForceHttp)), s.Data["insecureOCIForceHttp"])
|
||||
assert.Equal(t, []byte(repo.TLSClientCertData), s.Data["tlsClientCertData"])
|
||||
assert.Equal(t, []byte(repo.TLSClientCertKey), s.Data["tlsClientCertKey"])
|
||||
assert.Equal(t, []byte(repo.Type), s.Data["type"])
|
||||
assert.Equal(t, []byte(repo.GithubAppPrivateKey), s.Data["githubAppPrivateKey"])
|
||||
assert.Equal(t, []byte(strconv.FormatInt(repo.GithubAppId, 10)), s.Data["githubAppID"])
|
||||
assert.Equal(t, []byte(strconv.FormatInt(repo.GithubAppInstallationId, 10)), s.Data["githubAppInstallationID"])
|
||||
assert.Equal(t, []byte(repo.GitHubAppEnterpriseBaseURL), s.Data["githubAppEnterpriseBaseUrl"])
|
||||
assert.Equal(t, []byte(repo.Proxy), s.Data["proxy"])
|
||||
assert.Equal(t, []byte(repo.NoProxy), s.Data["noProxy"])
|
||||
assert.Equal(t, []byte(repo.Project), s.Data["project"])
|
||||
assert.Equal(t, []byte(repo.GCPServiceAccountKey), s.Data["gcpServiceAccountKey"])
|
||||
assert.Equal(t, []byte(strconv.FormatBool(repo.ForceHttpBasicAuth)), s.Data["forceHttpBasicAuth"])
|
||||
assert.Equal(t, []byte(strconv.FormatBool(repo.UseAzureWorkloadIdentity)), s.Data["useAzureWorkloadIdentity"])
|
||||
assert.Equal(t, []byte(strconv.FormatInt(repo.Depth, 10)), s.Data["depth"])
|
||||
assert.Equal(t, []byte(strconv.FormatBool(repo.WebhookManifestCacheWarmDisabled)), s.Data["webhookManifestCacheWarmDisabled"])
|
||||
assert.Equal(t, map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD}, s.Annotations)
|
||||
assert.Equal(t, map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository}, s.Labels)
|
||||
}
|
||||
|
||||
func TestCreateReadAndWriteRepoCredsSecretForSameURL(t *testing.T) {
|
||||
clientset := getClientset()
|
||||
settingsMgr := settings.NewSettingsManager(t.Context(), clientset, testNamespace)
|
||||
|
||||
@@ -22,6 +22,10 @@ func TestMain(m *testing.M) {
|
||||
// Ensure tests use non-cached proxy callback
|
||||
proxy.UseTestingProxyCallback()
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
os.Setenv("GIT_CONFIG_NOSYSTEM", "1")
|
||||
os.Setenv("GIT_CONFIG_GLOBAL", filepath.Join(cwd, "testdata", "gitconfig"))
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
|
||||
5
util/git/testdata/gitconfig
vendored
Normal file
5
util/git/testdata/gitconfig
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
[user]
|
||||
name = argo
|
||||
email = argoproj@example.com
|
||||
[init]
|
||||
defaultBranch = master
|
||||
@@ -1311,23 +1311,58 @@ func (mgr *SettingsManager) GetSettings() (*ArgoCDSettings, error) {
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// isRepositorySecret reports whether obj is a repository credential secret
|
||||
// (argocd.argoproj.io/secret-type=repository). Only repository credential changes
|
||||
// need to invalidate the project cache; cluster changes flow through the cluster
|
||||
// informer. Unwraps cache.DeletedFinalStateUnknown tombstones for DeleteFunc handlers.
|
||||
// Unknown types return false (fail-closed).
|
||||
func isRepositorySecret(obj any) bool {
|
||||
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
|
||||
obj = tombstone.Obj
|
||||
}
|
||||
repoSelector := labels.SelectorFromSet(labels.Set{common.LabelKeySecretType: common.LabelValueSecretTypeRepository})
|
||||
if s, ok := obj.(metav1.Object); ok {
|
||||
return repoSelector.Matches(labels.Set(s.GetLabels()))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isSettingsObject reports whether obj carries app.kubernetes.io/part-of=argocd,
|
||||
// the label that identifies secrets and configmaps that participate in ArgoCD's
|
||||
// settings system (OIDC config, webhook secrets, $secretName:key template references).
|
||||
// Unwraps cache.DeletedFinalStateUnknown tombstones for DeleteFunc handlers.
|
||||
// Unknown types return false (fail-closed).
|
||||
func isSettingsObject(obj any) bool {
|
||||
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
|
||||
obj = tombstone.Obj
|
||||
}
|
||||
settingsSelector := labels.SelectorFromSet(labels.Set{"app.kubernetes.io/part-of": "argocd"})
|
||||
if s, ok := obj.(metav1.Object); ok {
|
||||
return settingsSelector.Matches(labels.Set(s.GetLabels()))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isArgoCDConfigMap reports whether obj is the argocd-cm ConfigMap. Only argocd-cm
|
||||
// carries settings that affect project cache validity (the "globalProjects" key, read
|
||||
// by GetGlobalProjectsSettings). Unwraps cache.DeletedFinalStateUnknown tombstones for
|
||||
// DeleteFunc handlers. Unknown types return false (fail-closed).
|
||||
func isArgoCDConfigMap(obj any) bool {
|
||||
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
|
||||
obj = tombstone.Obj
|
||||
}
|
||||
if metaObj, ok := obj.(metav1.Object); ok {
|
||||
return metaObj.GetName() == common.ArgoCDConfigMapName
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (mgr *SettingsManager) initialize(ctx context.Context) error {
|
||||
tweakConfigMap := func(options *metav1.ListOptions) {
|
||||
cmLabelSelector := fields.ParseSelectorOrDie(partOfArgoCDSelector)
|
||||
options.LabelSelector = cmLabelSelector.String()
|
||||
}
|
||||
|
||||
eventHandler := cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: func(_, _ any) {
|
||||
mgr.onRepoOrClusterChanged()
|
||||
},
|
||||
AddFunc: func(_ any) {
|
||||
mgr.onRepoOrClusterChanged()
|
||||
},
|
||||
DeleteFunc: func(_ any) {
|
||||
mgr.onRepoOrClusterChanged()
|
||||
},
|
||||
}
|
||||
indexers := cache.Indexers{
|
||||
cache.NamespaceIndex: cache.MetaNamespaceIndexFunc,
|
||||
ByProjectRepoIndexer: byProjectIndexerFunc(common.LabelValueSecretTypeRepository),
|
||||
@@ -1342,17 +1377,65 @@ func (mgr *SettingsManager) initialize(ctx context.Context) error {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
_, err = cmInformer.AddEventHandler(eventHandler)
|
||||
// ConfigMap informer: filtered to app.kubernetes.io/part-of=argocd (see tweakConfigMap).
|
||||
// Only argocd-cm carries settings that affect project cache validity: the "globalProjects"
|
||||
// key controls which AppProjects are treated as global (merged into virtual projects via
|
||||
// GetGlobalProjectsSettings). Other part-of=argocd configmaps (argocd-rbac-cm, etc.) have
|
||||
// no path into project cache construction and don't need to trigger invalidation.
|
||||
_, err = cmInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: func(_, obj any) {
|
||||
if isArgoCDConfigMap(obj) {
|
||||
mgr.onRepoOrClusterChanged()
|
||||
}
|
||||
},
|
||||
AddFunc: func(obj any) {
|
||||
if isArgoCDConfigMap(obj) {
|
||||
mgr.onRepoOrClusterChanged()
|
||||
}
|
||||
},
|
||||
DeleteFunc: func(obj any) {
|
||||
if isArgoCDConfigMap(obj) {
|
||||
mgr.onRepoOrClusterChanged()
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
_, err = secretsInformer.AddEventHandler(eventHandler)
|
||||
// Secrets informer: filtered to argocd.argoproj.io/secret-type != cluster,
|
||||
// so cluster secrets are excluded (handled by the cluster informer below).
|
||||
// Only repository credential changes affect project-repo bindings and need
|
||||
// to invalidate the project cache.
|
||||
_, err = secretsInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: func(_, obj any) {
|
||||
if isRepositorySecret(obj) {
|
||||
mgr.onRepoOrClusterChanged()
|
||||
}
|
||||
},
|
||||
AddFunc: func(obj any) {
|
||||
if isRepositorySecret(obj) {
|
||||
mgr.onRepoOrClusterChanged()
|
||||
}
|
||||
},
|
||||
DeleteFunc: func(obj any) {
|
||||
if isRepositorySecret(obj) {
|
||||
mgr.onRepoOrClusterChanged()
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
_, err = clusterInformer.AddEventHandler(eventHandler)
|
||||
// Cluster informer: filtered to argocd.argoproj.io/secret-type=cluster,
|
||||
// so every event represents a cluster credential change, which always
|
||||
// warrants a settings reload.
|
||||
_, err = clusterInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: func(_, _ any) { mgr.onRepoOrClusterChanged() },
|
||||
AddFunc: func(_ any) { mgr.onRepoOrClusterChanged() },
|
||||
DeleteFunc: func(_ any) { mgr.onRepoOrClusterChanged() },
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
@@ -1389,19 +1472,28 @@ func (mgr *SettingsManager) initialize(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
now := time.Now()
|
||||
// handler notifies subscribers of settings changes. Guarded by isSettingsObject
|
||||
// so that only changes to app.kubernetes.io/part-of=argocd objects (the documented
|
||||
// contract for secrets/configmaps that participate in ArgoCD settings) trigger a
|
||||
// full GetSettings() reload. This prevents spurious reloads caused by the informer
|
||||
// resync period delivering synthetic UPDATE events for unrelated objects.
|
||||
handler := cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj any) {
|
||||
if metaObj, ok := obj.(metav1.Object); ok {
|
||||
if metaObj.GetCreationTimestamp().After(now) {
|
||||
tryNotify()
|
||||
if isSettingsObject(obj) {
|
||||
if metaObj, ok := obj.(metav1.Object); ok {
|
||||
if metaObj.GetCreationTimestamp().After(now) {
|
||||
tryNotify()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
UpdateFunc: func(oldObj, newObj any) {
|
||||
oldMeta, oldOk := oldObj.(metav1.Common)
|
||||
newMeta, newOk := newObj.(metav1.Common)
|
||||
if oldOk && newOk && oldMeta.GetResourceVersion() != newMeta.GetResourceVersion() {
|
||||
tryNotify()
|
||||
if isSettingsObject(newObj) {
|
||||
oldMeta, oldOk := oldObj.(metav1.Common)
|
||||
newMeta, newOk := newObj.(metav1.Common)
|
||||
if oldOk && newOk && oldMeta.GetResourceVersion() != newMeta.GetResourceVersion() {
|
||||
tryNotify()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
@@ -2435,3 +2436,169 @@ func TestSecretsInformerExcludesClusterSecrets(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsRepositorySecret(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj any
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "repository secret matches",
|
||||
obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
|
||||
}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "unlabeled secret does not match",
|
||||
obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{}},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "cluster secret does not match",
|
||||
obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeCluster},
|
||||
}},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "tombstone wrapping repository secret matches",
|
||||
obj: cache.DeletedFinalStateUnknown{
|
||||
Obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepository},
|
||||
}},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "tombstone wrapping non-repository secret does not match",
|
||||
obj: cache.DeletedFinalStateUnknown{
|
||||
Obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{}},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "unknown type does not match",
|
||||
obj: "unexpected-type",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, isRepositorySecret(tt.obj))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSettingsObject(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj any
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "secret with part-of=argocd matches",
|
||||
obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app.kubernetes.io/part-of": "argocd"},
|
||||
}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "unlabeled secret does not match",
|
||||
obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{}},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "secret with different part-of value does not match",
|
||||
obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app.kubernetes.io/part-of": "other-app"},
|
||||
}},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "configmap with part-of=argocd matches",
|
||||
obj: &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app.kubernetes.io/part-of": "argocd"},
|
||||
}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "tombstone wrapping labeled secret matches",
|
||||
obj: cache.DeletedFinalStateUnknown{
|
||||
Obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app.kubernetes.io/part-of": "argocd"},
|
||||
}},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "tombstone wrapping unlabeled secret does not match",
|
||||
obj: cache.DeletedFinalStateUnknown{
|
||||
Obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{}},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "unknown type does not match",
|
||||
obj: "unexpected-type",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, isSettingsObject(tt.obj))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsArgoCDConfigMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj any
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "argocd-cm matches",
|
||||
obj: &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDConfigMapName,
|
||||
}},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "other configmap does not match",
|
||||
obj: &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDRBACConfigMapName,
|
||||
}},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "tombstone wrapping argocd-cm matches",
|
||||
obj: cache.DeletedFinalStateUnknown{
|
||||
Obj: &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDConfigMapName,
|
||||
}},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "tombstone wrapping other configmap does not match",
|
||||
obj: cache.DeletedFinalStateUnknown{
|
||||
Obj: &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDRBACConfigMapName,
|
||||
}},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "unknown type does not match",
|
||||
obj: "unexpected-type",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, isArgoCDConfigMap(tt.obj))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"html"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -25,6 +26,8 @@ import (
|
||||
"github.com/go-playground/webhooks/v6/gitlab"
|
||||
"github.com/go-playground/webhooks/v6/gogs"
|
||||
gogsclient "github.com/gogits/go-gogs-client"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
@@ -55,6 +58,24 @@ const payloadQueueSize = 50000
|
||||
|
||||
const panicMsgServer = "panic while processing api-server webhook event"
|
||||
|
||||
var (
|
||||
webhookManifestCacheWarmDisabled = os.Getenv("ARGOCD_WEBHOOK_MANIFEST_CACHE_WARM_DISABLED") == "true"
|
||||
webhookRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_webhook_requests_total",
|
||||
Help: "Number of webhook requests received by repo.",
|
||||
}, []string{"repo"})
|
||||
|
||||
webhookStoreCacheAttemptsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_webhook_store_cache_attempts_total",
|
||||
Help: "Number of attempts to store previously cached manifests triggered by a webhook event.",
|
||||
}, []string{"repo", "successful"})
|
||||
|
||||
webhookHandlersInFlight = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "argocd_webhook_handlers_in_flight",
|
||||
Help: "Number of webhook HandleEvent calls currently in progress.",
|
||||
})
|
||||
)
|
||||
|
||||
var _ settingsSource = &settings.SettingsManager{}
|
||||
|
||||
type ArgoCDWebhookHandler struct {
|
||||
@@ -313,6 +334,15 @@ type changeInfo struct {
|
||||
|
||||
// HandleEvent handles webhook events for repo push events
|
||||
func (a *ArgoCDWebhookHandler) HandleEvent(payload any) {
|
||||
webhookHandlersInFlight.Inc()
|
||||
defer webhookHandlersInFlight.Dec()
|
||||
|
||||
start := time.Now()
|
||||
log.Info("Webhook handler started")
|
||||
defer func() {
|
||||
log.Infof("Webhook handler completed in %v", time.Since(start))
|
||||
}()
|
||||
|
||||
webURLs, revision, change, touchedHead, changedFiles := a.affectedRevisionInfo(payload)
|
||||
// NOTE: the webURL does not include the .git extension
|
||||
if len(webURLs) == 0 {
|
||||
@@ -321,6 +351,7 @@ func (a *ArgoCDWebhookHandler) HandleEvent(payload any) {
|
||||
}
|
||||
for _, webURL := range webURLs {
|
||||
log.Infof("Received push event repo: %s, revision: %s, touchedHead: %v", webURL, revision, touchedHead)
|
||||
webhookRequestsTotal.WithLabelValues(git.NormalizeGitURL(webURL)).Inc()
|
||||
}
|
||||
|
||||
nsFilter := a.ns
|
||||
@@ -368,6 +399,16 @@ func (a *ArgoCDWebhookHandler) HandleEvent(payload any) {
|
||||
continue
|
||||
}
|
||||
|
||||
cacheWarmDisabled := webhookManifestCacheWarmDisabled
|
||||
if !cacheWarmDisabled {
|
||||
repo, err := a.lookupRepository(context.Background(), webURL)
|
||||
if err != nil {
|
||||
log.Debugf("Failed to look up repository for %s: %v", webURL, err)
|
||||
} else if repo != nil && repo.WebhookManifestCacheWarmDisabled {
|
||||
cacheWarmDisabled = true
|
||||
}
|
||||
}
|
||||
|
||||
// iterate over apps and check if any files specified in their sources have changed
|
||||
for _, app := range filteredApps {
|
||||
// get all sources, including sync source and dry source if source hydrator is configured
|
||||
@@ -401,10 +442,13 @@ func (a *ArgoCDWebhookHandler) HandleEvent(payload any) {
|
||||
log.Errorf("Failed to refresh app '%s': %v", app.Name, err)
|
||||
}
|
||||
break // we don't need to check other sources
|
||||
} else if change.shaBefore != "" && change.shaAfter != "" {
|
||||
} else if change.shaBefore != "" && change.shaAfter != "" && !cacheWarmDisabled {
|
||||
// update the cached manifests with the new revision cache key
|
||||
if err := a.storePreviouslyCachedManifests(&app, change, trackingMethod, appInstanceLabelKey, installationID, source); err != nil {
|
||||
log.Errorf("Failed to store cached manifests of previous revision for app '%s': %v", app.Name, err)
|
||||
log.Debugf("Failed to store cached manifests of previous revision for app '%s': %v", app.Name, err)
|
||||
webhookStoreCacheAttemptsTotal.WithLabelValues(git.NormalizeGitURL(webURL), "false").Inc()
|
||||
} else {
|
||||
webhookStoreCacheAttemptsTotal.WithLabelValues(git.NormalizeGitURL(webURL), "true").Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,13 +70,25 @@ type reactorDef struct {
|
||||
reaction kubetesting.ReactionFunc
|
||||
}
|
||||
|
||||
func assertLogContains(t *testing.T, hook *test.Hook, msg string) {
|
||||
t.Helper()
|
||||
for _, entry := range hook.Entries {
|
||||
if entry.Message == msg {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("log hook did not contain message: %q", msg)
|
||||
}
|
||||
|
||||
func NewMockHandler(reactor *reactorDef, applicationNamespaces []string, objects ...runtime.Object) *ArgoCDWebhookHandler {
|
||||
defaultMaxPayloadSize := int64(50) * 1024 * 1024
|
||||
return NewMockHandlerWithPayloadLimit(reactor, applicationNamespaces, defaultMaxPayloadSize, objects...)
|
||||
}
|
||||
|
||||
func NewMockHandlerWithPayloadLimit(reactor *reactorDef, applicationNamespaces []string, maxPayloadSize int64, objects ...runtime.Object) *ArgoCDWebhookHandler {
|
||||
return newMockHandler(reactor, applicationNamespaces, maxPayloadSize, &mocks.ArgoDB{}, &settings.ArgoCDSettings{}, objects...)
|
||||
mockDB := &mocks.ArgoDB{}
|
||||
mockDB.EXPECT().ListRepositories(mock.Anything).Return([]*v1alpha1.Repository{}, nil).Maybe()
|
||||
return newMockHandler(reactor, applicationNamespaces, maxPayloadSize, mockDB, &settings.ArgoCDSettings{}, objects...)
|
||||
}
|
||||
|
||||
func NewMockHandlerForBitbucketCallback(reactor *reactorDef, applicationNamespaces []string, objects ...runtime.Object) *ArgoCDWebhookHandler {
|
||||
@@ -161,7 +173,7 @@ func TestGitHubCommitEvent(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
expectedLogResult := "Received push event repo: https://github.com/jessesuen/test-repo, revision: master, touchedHead: true"
|
||||
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResult)
|
||||
hook.Reset()
|
||||
}
|
||||
|
||||
@@ -179,7 +191,7 @@ func TestAzureDevOpsCommitEvent(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
expectedLogResult := "Received push event repo: https://dev.azure.com/alexander0053/alex-test/_git/alex-test, revision: master, touchedHead: true"
|
||||
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResult)
|
||||
hook.Reset()
|
||||
}
|
||||
|
||||
@@ -295,7 +307,7 @@ func TestGitHubTagEvent(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
expectedLogResult := "Received push event repo: https://github.com/jessesuen/test-repo, revision: v1.0, touchedHead: false"
|
||||
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResult)
|
||||
hook.Reset()
|
||||
}
|
||||
|
||||
@@ -313,7 +325,7 @@ func TestGitHubPingEvent(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
expectedLogResult := "Ignoring webhook event"
|
||||
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResult)
|
||||
hook.Reset()
|
||||
}
|
||||
|
||||
@@ -331,9 +343,9 @@ func TestBitbucketServerRepositoryReferenceChangedEvent(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
expectedLogResultSSH := "Received push event repo: ssh://git@bitbucketserver:7999/myproject/test-repo.git, revision: master, touchedHead: true"
|
||||
assert.Equal(t, expectedLogResultSSH, hook.AllEntries()[len(hook.AllEntries())-2].Message)
|
||||
assertLogContains(t, hook, expectedLogResultSSH)
|
||||
expectedLogResultHTTPS := "Received push event repo: https://bitbucketserver/scm/myproject/test-repo.git, revision: master, touchedHead: true"
|
||||
assert.Equal(t, expectedLogResultHTTPS, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResultHTTPS)
|
||||
hook.Reset()
|
||||
}
|
||||
|
||||
@@ -349,7 +361,7 @@ func TestBitbucketServerRepositoryDiagnosticPingEvent(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
expectedLogResult := "Ignoring webhook event"
|
||||
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResult)
|
||||
hook.Reset()
|
||||
}
|
||||
|
||||
@@ -367,7 +379,7 @@ func TestGogsPushEvent(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
expectedLogResult := "Received push event repo: http://gogs-server/john/repo-test, revision: master, touchedHead: true"
|
||||
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResult)
|
||||
hook.Reset()
|
||||
}
|
||||
|
||||
@@ -385,7 +397,7 @@ func TestGitLabPushEvent(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
expectedLogResult := "Received push event repo: https://gitlab.com/group/name, revision: master, touchedHead: true"
|
||||
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResult)
|
||||
hook.Reset()
|
||||
}
|
||||
|
||||
@@ -403,7 +415,7 @@ func TestGitLabSystemEvent(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
expectedLogResult := "Received push event repo: https://gitlab.com/group/name, revision: master, touchedHead: true"
|
||||
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResult)
|
||||
hook.Reset()
|
||||
}
|
||||
|
||||
@@ -418,7 +430,7 @@ func TestInvalidMethod(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
||||
expectedLogResult := "Webhook processing failed: invalid HTTP Method"
|
||||
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResult)
|
||||
assert.Equal(t, expectedLogResult+"\n", w.Body.String())
|
||||
hook.Reset()
|
||||
}
|
||||
@@ -434,7 +446,7 @@ func TestInvalidEvent(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
expectedLogResult := "Webhook processing failed: The payload is either too large or corrupted. Please check the payload size (must be under 50 MB) and ensure it is valid JSON"
|
||||
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResult)
|
||||
assert.Equal(t, expectedLogResult+"\n", w.Body.String())
|
||||
hook.Reset()
|
||||
}
|
||||
@@ -752,7 +764,7 @@ func TestGitHubCommitEventMaxPayloadSize(t *testing.T) {
|
||||
h.Wait()
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
expectedLogResult := "Webhook processing failed: The payload is either too large or corrupted. Please check the payload size (must be under 0 MB) and ensure it is valid JSON"
|
||||
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
|
||||
assertLogContains(t, hook, expectedLogResult)
|
||||
hook.Reset()
|
||||
}
|
||||
|
||||
@@ -1119,6 +1131,7 @@ func TestHandleEvent(t *testing.T) {
|
||||
APIVersions: []string{},
|
||||
},
|
||||
}, nil).Maybe()
|
||||
mockDB.EXPECT().ListRepositories(mock.Anything).Return([]*v1alpha1.Repository{}, nil).Maybe()
|
||||
|
||||
err := serverCache.SetClusterInfo(testClusterURL, &v1alpha1.ClusterInfo{
|
||||
ServerVersion: "1.28.0",
|
||||
|
||||
Reference in New Issue
Block a user