Compare commits

...

14 Commits

Author SHA1 Message Date
argo-cd-cherry-pick-bot[bot]
12c8d42f4f chore: use base ref for cherry-pick prs (cherry-pick #26551 for 3.1) (#26555)
Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>
Co-authored-by: Blake Pettersson <blake.pettersson@gmail.com>
2026-02-22 21:34:49 +02:00
nmirasch
c19d63446d chore(deps): bump lodash from 4.17.21 to 4.17.23 (Cherry-pick 3.1) (#26210)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-10 17:49:44 +02:00
github-actions[bot]
a46152e850 Bump version to 3.1.12 on release-3.1 branch (#26117)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: crenshaw-dev <350466+crenshaw-dev@users.noreply.github.com>
2026-01-22 14:34:09 -05:00
Codey Jenkins
28b618775d fix: cherry pick #25516 to release-3.1 (#26116)
Signed-off-by: Codey Jenkins <FourFifthsCode@users.noreply.github.com>
Signed-off-by: pbhatnagar-oss <pbhatifiwork@gmail.com>
Co-authored-by: pbhatnagar-oss <pbhatifiwork@gmail.com>
Co-authored-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
2026-01-22 13:33:42 -05:00
dudinea
1b7cfc0c06 fix: invalid error message on health check failure (#26040) (cherry pick #26039 for 3.1) (#26071)
Signed-off-by: Eugene Doudine <eugene.doudine@octopus.com>
2026-01-20 18:35:15 +02:00
argo-cd-cherry-pick-bot[bot]
0262c8ff97 fix(hydrator): empty links for failed operation (#25025) (cherry-pick #26014 for 3.1) (#26017)
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2026-01-15 16:26:23 -05:00
Nitish Kumar
cc053b2eeb fix(ci): bump golang.x/tools to v0.35.0 (#25955)
Signed-off-by: nitishfy <justnitish06@gmail.com>
2026-01-13 13:03:04 +02:00
github-actions[bot]
f9adb4e7f4 Bump version to 3.1.11 on release-3.1 branch (#25953)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: reggie-k <19544836+reggie-k@users.noreply.github.com>
2026-01-13 09:59:27 +02:00
Keith Chong
6c3f3eab5d fix: Selecting repoType in dropdown doesn't do anything (#23747) (#25945)
Signed-off-by: Keith Chong <kykchong@redhat.com>
2026-01-13 08:25:28 +01:00
Nitish Kumar
f22ccc2192 chore(cherry-pick-3.1): bump go to 1.25.5 and bump expr to v1.17.7 (#25890)
Signed-off-by: nitishfy <justnitish06@gmail.com>
2026-01-12 10:57:44 -05:00
argo-cd-cherry-pick-bot[bot]
b424210b52 fix(appset): do not trigger reconciliation on appsets not part of allowed namespaces when updating a cluster secret (cherry-pick #25622 for 3.1) (#25910)
Signed-off-by: OpenGuidou <guillaume.doussin@gmail.com>
Co-authored-by: OpenGuidou <73480729+OpenGuidou@users.noreply.github.com>
2026-01-09 16:18:10 +01:00
Atif Ali
955ea1b1df chore(cherry-pick-3.1): bump expr version from v1.17.5 to v1.17.7 (#25907)
Signed-off-by: Atif Ali <atali@redhat.com>
2026-01-08 13:44:26 -05:00
argo-cd-cherry-pick-bot[bot]
836d47f311 fix: Only show please update resource specification message when spec… (cherry-pick #25066 for 3.1) (#25896)
Signed-off-by: Josh Soref <jsoref@gmail.com>
Co-authored-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2026-01-07 10:11:13 -05:00
argo-cd-cherry-pick-bot[bot]
db2004f2e2 ci: test against k8s 1.34.2 (cherry-pick #25856 for 3.1) (#25861)
Signed-off-by: reggie-k <regina.voloshin@codefresh.io>
Co-authored-by: Regina Voloshin <regina.voloshin@codefresh.io>
2026-01-05 13:40:38 -05:00
49 changed files with 976 additions and 455 deletions

View File

@@ -14,7 +14,7 @@ on:
env:
# Golang version to use across CI steps
# renovate: datasource=golang-version packageName=golang
GOLANG_VERSION: '1.24.6'
GOLANG_VERSION: '1.25.5'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -103,16 +103,16 @@ jobs:
- changes
steps:
- name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Run golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
# renovate: datasource=go packageName=github.com/golangci/golangci-lint versioning=regex:^v(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)?$
version: v2.1.6
version: v2.5.0
args: --verbose
test-go:
@@ -414,14 +414,14 @@ jobs:
# latest: true means that this version mush upload the coverage report to codecov.io
# We designate the latest version because we only collect code coverage for that version.
k3s:
- version: v1.33.1
- version: v1.34.2
latest: true
- version: v1.33.1
latest: false
- version: v1.32.1
latest: false
- version: v1.31.0
latest: false
- version: v1.30.4
latest: false
needs:
- build-go
- changes

View File

@@ -53,7 +53,7 @@ jobs:
with:
# Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations)
# renovate: datasource=golang-version packageName=golang
go-version: 1.24.6
go-version: 1.25.5
platforms: ${{ needs.set-vars.outputs.platforms }}
push: false
@@ -70,7 +70,7 @@ jobs:
ghcr_image_name: ghcr.io/argoproj/argo-cd/argocd:${{ needs.set-vars.outputs.image-tag }}
# Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations)
# renovate: datasource=golang-version packageName=golang
go-version: 1.24.6
go-version: 1.25.5
platforms: ${{ needs.set-vars.outputs.platforms }}
push: true
secrets:

View File

@@ -11,7 +11,7 @@ permissions: {}
env:
# renovate: datasource=golang-version packageName=golang
GOLANG_VERSION: '1.24.6' # Note: go-version must also be set in job argocd-image.with.go-version
GOLANG_VERSION: '1.25.5' # Note: go-version must also be set in job argocd-image.with.go-version
jobs:
argocd-image:
@@ -25,7 +25,7 @@ jobs:
quay_image_name: quay.io/argoproj/argocd:${{ github.ref_name }}
# Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations)
# renovate: datasource=golang-version packageName=golang
go-version: 1.24.6
go-version: 1.25.5
platforms: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le
push: true
secrets:

View File

@@ -4,7 +4,7 @@ ARG BASE_IMAGE=docker.io/library/ubuntu:24.04@sha256:80dd3c3b9c6cecb9f1667e9290b
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final image
# Also used as the image in CI jobs so needs all dependencies
####################################################################################################
FROM docker.io/library/golang:1.24.6@sha256:2c89c41fb9efc3807029b59af69645867cfe978d2b877d475be0d72f6c6ce6f6 AS builder
FROM docker.io/library/golang:1.25.5@sha256:6cc2338c038bc20f96ab32848da2b5c0641bb9bb5363f2c33e9b7c8838f9a208 AS builder
WORKDIR /tmp
@@ -103,7 +103,7 @@ RUN HOST_ARCH=$TARGETARCH NODE_ENV='production' NODE_ONLINE_ENV='online' NODE_OP
####################################################################################################
# Argo CD Build stage which performs the actual build of Argo CD binaries
####################################################################################################
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.24.6@sha256:2c89c41fb9efc3807029b59af69645867cfe978d2b877d475be0d72f6c6ce6f6 AS argocd-build
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.25.5@sha256:6cc2338c038bc20f96ab32848da2b5c0641bb9bb5363f2c33e9b7c8838f9a208 AS argocd-build
WORKDIR /go/src/github.com/argoproj/argo-cd

View File

@@ -1 +1 @@
3.1.10
3.1.12

View File

@@ -576,8 +576,9 @@ func (r *ApplicationSetReconciler) SetupWithManager(mgr ctrl.Manager, enableProg
Watches(
&corev1.Secret{},
&clusterSecretEventHandler{
Client: mgr.GetClient(),
Log: log.WithField("type", "createSecretEventHandler"),
Client: mgr.GetClient(),
Log: log.WithField("type", "createSecretEventHandler"),
ApplicationSetNamespaces: r.ApplicationSetNamespaces,
}).
Complete(r)
}

View File

@@ -14,6 +14,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
"github.com/argoproj/argo-cd/v3/common"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)
@@ -22,8 +23,9 @@ import (
// requeue any related ApplicationSets.
type clusterSecretEventHandler struct {
// handler.EnqueueRequestForOwner
Log log.FieldLogger
Client client.Client
Log log.FieldLogger
Client client.Client
ApplicationSetNamespaces []string
}
func (h *clusterSecretEventHandler) Create(ctx context.Context, e event.CreateEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
@@ -68,6 +70,10 @@ func (h *clusterSecretEventHandler) queueRelatedAppGenerators(ctx context.Contex
h.Log.WithField("count", len(appSetList.Items)).Info("listed ApplicationSets")
for _, appSet := range appSetList.Items {
if !utils.IsNamespaceAllowed(h.ApplicationSetNamespaces, appSet.GetNamespace()) {
// Ignore it as not part of the allowed list of namespaces in which to watch Appsets
continue
}
foundClusterGenerator := false
for _, generator := range appSet.Spec.Generators {
if generator.Clusters != nil {

View File

@@ -137,7 +137,7 @@ func TestClusterEventHandler(t *testing.T) {
{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app-set",
Namespace: "another-namespace",
Namespace: "argocd",
},
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{
@@ -171,9 +171,37 @@ func TestClusterEventHandler(t *testing.T) {
},
},
expectedRequests: []reconcile.Request{
{NamespacedName: types.NamespacedName{Namespace: "another-namespace", Name: "my-app-set"}},
{NamespacedName: types.NamespacedName{Namespace: "argocd", Name: "my-app-set"}},
},
},
{
name: "cluster generators in other namespaces should not match",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app-set",
Namespace: "my-namespace-not-allowed",
},
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{
{
Clusters: &argov1alpha1.ClusterGenerator{},
},
},
},
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
},
},
},
expectedRequests: []reconcile.Request{},
},
{
name: "non-argo cd secret should not match",
items: []argov1alpha1.ApplicationSet{
@@ -552,8 +580,9 @@ func TestClusterEventHandler(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(&appSetList).Build()
handler := &clusterSecretEventHandler{
Client: fakeClient,
Log: log.WithField("type", "createSecretEventHandler"),
Client: fakeClient,
Log: log.WithField("type", "createSecretEventHandler"),
ApplicationSetNamespaces: []string{"argocd"},
}
mockAddRateLimitingInterface := mockAddRateLimitingInterface{}

View File

@@ -24,7 +24,7 @@ func extractHealthStatusAndReason(node v1alpha1.ResourceNode) (healthStatus heal
healthStatus = node.Health.Status
reason = node.Health.Message
}
return
return healthStatus, reason
}
func treeViewAppGet(prefix string, uidToNodeMap map[string]v1alpha1.ResourceNode, parentToChildMap map[string][]string, parent v1alpha1.ResourceNode, mapNodeNameToResourceState map[string]*resourceState, w *tabwriter.Writer) {

View File

@@ -996,7 +996,7 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b
appKey, shutdown := ctrl.appOperationQueue.Get()
if shutdown {
processNext = false
return
return processNext
}
processNext = true
defer func() {
@@ -1009,16 +1009,16 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(appKey)
if err != nil {
log.Errorf("Failed to get application '%s' from informer index: %+v", appKey, err)
return
return processNext
}
if !exists {
// This happens after app was deleted, but the work queue still had an entry for it.
return
return processNext
}
origApp, ok := obj.(*appv1.Application)
if !ok {
log.Warnf("Key '%s' in index is not an application", appKey)
return
return processNext
}
app := origApp.DeepCopy()
logCtx := log.WithFields(applog.GetAppLogFields(app))
@@ -1038,7 +1038,7 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b
freshApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.ObjectMeta.Namespace).Get(context.Background(), app.Name, metav1.GetOptions{})
if err != nil {
logCtx.Errorf("Failed to retrieve latest application state: %v", err)
return
return processNext
}
app = freshApp
}
@@ -1060,7 +1060,7 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b
}
ts.AddCheckpoint("finalize_application_deletion_ms")
}
return
return processNext
}
func (ctrl *ApplicationController) processAppComparisonTypeQueueItem() (processNext bool) {
@@ -1075,7 +1075,7 @@ func (ctrl *ApplicationController) processAppComparisonTypeQueueItem() (processN
}()
if shutdown {
processNext = false
return
return processNext
}
if parts := strings.Split(key, "/"); len(parts) != 3 {
@@ -1084,11 +1084,11 @@ func (ctrl *ApplicationController) processAppComparisonTypeQueueItem() (processN
compareWith, err := strconv.Atoi(parts[2])
if err != nil {
log.Warnf("Unable to parse comparison type: %v", err)
return
return processNext
}
ctrl.requestAppRefresh(ctrl.toAppQualifiedName(parts[1], parts[0]), CompareWith(compareWith).Pointer(), nil)
}
return
return processNext
}
func (ctrl *ApplicationController) processProjectQueueItem() (processNext bool) {
@@ -1103,21 +1103,21 @@ func (ctrl *ApplicationController) processProjectQueueItem() (processNext bool)
}()
if shutdown {
processNext = false
return
return processNext
}
obj, exists, err := ctrl.projInformer.GetIndexer().GetByKey(key)
if err != nil {
log.Errorf("Failed to get project '%s' from informer index: %+v", key, err)
return
return processNext
}
if !exists {
// This happens after appproj was deleted, but the work queue still had an entry for it.
return
return processNext
}
origProj, ok := obj.(*appv1.AppProject)
if !ok {
log.Warnf("Key '%s' in index is not an appproject", key)
return
return processNext
}
if origProj.DeletionTimestamp != nil && origProj.HasFinalizer() {
@@ -1125,7 +1125,7 @@ func (ctrl *ApplicationController) processProjectQueueItem() (processNext bool)
log.Warnf("Failed to finalize project deletion: %v", err)
}
}
return
return processNext
}
func (ctrl *ApplicationController) finalizeProjectDeletion(proj *appv1.AppProject) error {
@@ -1618,7 +1618,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
appKey, shutdown := ctrl.appRefreshQueue.Get()
if shutdown {
processNext = false
return
return processNext
}
processNext = true
defer func() {
@@ -1633,22 +1633,22 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(appKey)
if err != nil {
log.Errorf("Failed to get application '%s' from informer index: %+v", appKey, err)
return
return processNext
}
if !exists {
// This happens after app was deleted, but the work queue still had an entry for it.
return
return processNext
}
origApp, ok := obj.(*appv1.Application)
if !ok {
log.Warnf("Key '%s' in index is not an application", appKey)
return
return processNext
}
origApp = origApp.DeepCopy()
needRefresh, refreshType, comparisonLevel := ctrl.needRefreshAppStatus(origApp, ctrl.statusRefreshTimeout, ctrl.statusHardRefreshTimeout)
if !needRefresh {
return
return processNext
}
app := origApp.DeepCopy()
logCtx := log.WithFields(applog.GetAppLogFields(app)).WithFields(log.Fields{
@@ -1691,12 +1691,12 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
app.Status.Summary = tree.GetSummary(app)
if err := ctrl.cache.SetAppResourcesTree(app.InstanceName(ctrl.namespace), tree); err != nil {
logCtx.Errorf("Failed to cache resources tree: %v", err)
return
return processNext
}
}
patchDuration = ctrl.persistAppStatus(origApp, &app.Status)
return
return processNext
}
logCtx.Warnf("Failed to get cached managed resources for tree reconciliation, fall back to full reconciliation")
}
@@ -1718,14 +1718,14 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
logCtx.Warnf("failed to set app managed resources tree: %v", err)
}
ts.AddCheckpoint("process_refresh_app_conditions_errors_ms")
return
return processNext
}
destCluster, err = argo.GetDestinationCluster(context.Background(), app.Spec.Destination, ctrl.db)
if err != nil {
logCtx.Errorf("Failed to get destination cluster: %v", err)
// exit the reconciliation. ctrl.refreshAppConditions should have caught the error
return
return processNext
}
var localManifests []string
@@ -1766,7 +1766,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
if stderrors.Is(err, ErrCompareStateRepo) {
logCtx.Warnf("Ignoring temporary failed attempt to compare app state against repo: %v", err)
return // short circuit if git error is encountered
return processNext // short circuit if git error is encountered
}
for k, v := range compareResult.timings {
@@ -1835,14 +1835,14 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
}
}
ts.AddCheckpoint("process_finalizers_ms")
return
return processNext
}
func (ctrl *ApplicationController) processAppHydrateQueueItem() (processNext bool) {
appKey, shutdown := ctrl.appHydrateQueue.Get()
if shutdown {
processNext = false
return
return processNext
}
processNext = true
defer func() {
@@ -1854,29 +1854,29 @@ func (ctrl *ApplicationController) processAppHydrateQueueItem() (processNext boo
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(appKey)
if err != nil {
log.Errorf("Failed to get application '%s' from informer index: %+v", appKey, err)
return
return processNext
}
if !exists {
// This happens after app was deleted, but the work queue still had an entry for it.
return
return processNext
}
origApp, ok := obj.(*appv1.Application)
if !ok {
log.Warnf("Key '%s' in index is not an application", appKey)
return
return processNext
}
ctrl.hydrator.ProcessAppHydrateQueueItem(origApp)
log.WithFields(applog.GetAppLogFields(origApp)).Debug("Successfully processed app hydrate queue item")
return
return processNext
}
func (ctrl *ApplicationController) processHydrationQueueItem() (processNext bool) {
hydrationKey, shutdown := ctrl.hydrationQueue.Get()
if shutdown {
processNext = false
return
return processNext
}
processNext = true
defer func() {
@@ -1897,7 +1897,7 @@ func (ctrl *ApplicationController) processHydrationQueueItem() (processNext bool
ctrl.hydrator.ProcessHydrationQueueItem(hydrationKey)
logCtx.Debug("Successfully processed hydration queue item")
return
return processNext
}
func resourceStatusKey(res appv1.ResourceStatus) string {
@@ -2060,11 +2060,11 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
&appv1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: newAnnotations}, Status: *newStatus})
if err != nil {
logCtx.Errorf("Error constructing app status patch: %v", err)
return
return patchDuration
}
if !modified {
logCtx.Infof("No status changes. Skipping patch")
return
return patchDuration
}
// calculate time for path call
start := time.Now()

View File

@@ -146,7 +146,7 @@ func (h *Hydrator) ProcessHydrationQueueItem(hydrationKey HydrationQueueKey) (pr
logCtx = logCtx.WithFields(applog.GetAppLogFields(app))
logCtx.Errorf("Failed to hydrate app: %v", err)
}
return
return processNext
}
logCtx.WithField("appCount", len(relevantApps)).Debug("Successfully hydrated apps")
finishedAt := metav1.Now()
@@ -174,7 +174,7 @@ func (h *Hydrator) ProcessHydrationQueueItem(hydrationKey HydrationQueueKey) (pr
logCtx.WithField("app", app.QualifiedName()).WithError(err).Error("Failed to request app refresh after hydration")
}
}
return
return processNext
}
func (h *Hydrator) hydrateAppsLatestCommit(logCtx *log.Entry, hydrationKey HydrationQueueKey) ([]*appv1.Application, string, string, error) {

View File

@@ -267,7 +267,7 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
Revision: revision,
SyncedRevision: syncedRevision,
NoRevisionCache: noRevisionCache,
Paths: path.GetAppRefreshPaths(app),
Paths: path.GetSourceRefreshPaths(app, source),
AppLabelKey: appLabelKey,
AppName: app.InstanceName(m.namespace),
Namespace: appNamespace,

View File

@@ -1,5 +1,5 @@
| Argo CD version | Kubernetes versions |
|-----------------|---------------------|
| 3.1 | v1.33, v1.32, v1.31, v1.30 |
| 3.1 | v1.34, v1.33, v1.32, v1.31 |
| 3.0 | v1.32, v1.31, v1.30, v1.29 |
| 2.14 | v1.31, v1.30, v1.29, v1.28 |

2
go.mod
View File

@@ -29,7 +29,7 @@ require (
github.com/dlclark/regexp2 v1.11.5
github.com/dustin/go-humanize v1.0.1
github.com/evanphx/json-patch v5.9.11+incompatible
github.com/expr-lang/expr v1.17.5
github.com/expr-lang/expr v1.17.7
github.com/felixge/httpsnoop v1.0.4
github.com/fsnotify/fsnotify v1.9.0
github.com/gfleury/go-bitbucket-v1 v0.0.0-20240917142304-df385efaac68

4
go.sum
View File

@@ -257,8 +257,8 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
github.com/expr-lang/expr v1.17.5 h1:i1WrMvcdLF249nSNlpQZN1S6NXuW9WaOfF5tPi3aw3k=
github.com/expr-lang/expr v1.17.5/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=

View File

@@ -6,20 +6,20 @@ SRCROOT="$( CDPATH='' cd -- "$(dirname "$0")/../.." && pwd -P )"
# This script installs all our golang-based codegen utility CLIs necessary for codegen.
# Some dependencies are vendored in go.mod (ones which are actually imported in our codebase).
# Other dependencies are only used as a CLI and do not need vendoring in go.mod (doing so adds
# unecessary dependencies to go.mod). We want to maintain a single source of truth for versioning
# unnecessary dependencies to go.mod). We want to maintain a single source of truth for versioning
# our binaries (either go.mod or go install <pkg>@<version>), so we use two techniques to install
# our CLIs:
# 1. For CLIs which are NOT vendored in go.mod, we can run `go install <pkg>@<version>` with an explicit version
# 2. For packages which we *do* vendor in go.mod, we determine version from go.mod followed by `go install` with that version
go_mod_install() {
module=$(go list -f '{{.Module}}' $1 | awk '{print $1}')
module_version=$(go list -m $module | awk '{print $NF}' | head -1)
go install $1@$module_version
module=$(go list -f '{{.Module}}' "$1" | awk '{print $1}')
module_version=$(go list -m "$module" | awk '{print $NF}' | head -1)
go install "$1@$module_version"
}
# All binaries are compiled into the argo-cd/dist directory, which is added to the PATH during codegen
export GOBIN="${SRCROOT}/dist"
mkdir -p $GOBIN
mkdir -p "$GOBIN"
# protoc-gen-go* is used to generate <service>.pb.go from .proto files
# go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.0
@@ -41,8 +41,9 @@ go_mod_install k8s.io/code-generator/cmd/defaulter-gen
go_mod_install k8s.io/code-generator/cmd/informer-gen
go_mod_install k8s.io/code-generator/cmd/lister-gen
# We still install openapi-gen from go.mod since upstream does not utilize release tags
go_mod_install k8s.io/kube-openapi/cmd/openapi-gen
# We still install openapi-gen from go.mod since upstream does not utilize release tags. Use go install in order for
# replace directives to be respected.
go install k8s.io/kube-openapi/cmd/openapi-gen
# controller-gen is run by ./hack/gen-crd-spec to generate the CRDs
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.18.0
@@ -51,7 +52,7 @@ go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.18.0
go install github.com/go-swagger/go-swagger/cmd/swagger@v0.28.0
# goimports is used to auto-format generated code
go install golang.org/x/tools/cmd/goimports@v0.1.8
go install golang.org/x/tools/cmd/goimports@v0.35.0
# mockery is used to generate mock
go install github.com/vektra/mockery/v3@v3.3.6

View File

@@ -2,6 +2,6 @@
set -eux -o pipefail
# renovate: datasource=go packageName=github.com/golangci/golangci-lint
GOLANGCI_LINT_VERSION=2.1.6
GOLANGCI_LINT_VERSION=2.8.0
GO111MODULE=on go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v${GOLANGCI_LINT_VERSION}"
GO111MODULE=on go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v${GOLANGCI_LINT_VERSION}"

View File

@@ -18,6 +18,10 @@ IMAGE_TAG="${IMAGE_TAG:-}"
# if the tag has not been declared, and we are on a release branch, use the VERSION file.
if [ "$IMAGE_TAG" = "" ]; then
branch=$(git rev-parse --abbrev-ref HEAD)
# In GitHub Actions PRs, HEAD is detached; use GITHUB_BASE_REF (the target branch) instead
if [ "$branch" = "HEAD" ] && [ -n "${GITHUB_BASE_REF:-}" ]; then
branch="$GITHUB_BASE_REF"
fi
if [[ $branch = release-* ]]; then
pwd
IMAGE_TAG=v$(cat $SRCROOT/VERSION)

View File

@@ -12,4 +12,4 @@ resources:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v3.1.10
newTag: v3.1.12

View File

@@ -5,7 +5,7 @@ kind: Kustomization
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v3.1.10
newTag: v3.1.12
resources:
- ./application-controller
- ./dex

View File

@@ -24705,7 +24705,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -24831,7 +24831,7 @@ spec:
key: log.format.timestamp
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -24959,7 +24959,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -25250,7 +25250,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -25302,7 +25302,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -25644,7 +25644,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -24673,7 +24673,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -24793,7 +24793,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -25084,7 +25084,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -25136,7 +25136,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -25478,7 +25478,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -12,4 +12,4 @@ resources:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v3.1.10
newTag: v3.1.12

View File

@@ -12,7 +12,7 @@ patches:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v3.1.10
newTag: v3.1.12
resources:
- ../../base/application-controller
- ../../base/applicationset-controller

View File

@@ -26071,7 +26071,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -26197,7 +26197,7 @@ spec:
key: log.format.timestamp
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -26348,7 +26348,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -26444,7 +26444,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -26568,7 +26568,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -26885,7 +26885,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -26937,7 +26937,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -27311,7 +27311,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -27689,7 +27689,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -26041,7 +26041,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -26184,7 +26184,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -26280,7 +26280,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -26404,7 +26404,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -26721,7 +26721,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -26773,7 +26773,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -27147,7 +27147,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -27525,7 +27525,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -1874,7 +1874,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -2000,7 +2000,7 @@ spec:
key: log.format.timestamp
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2151,7 +2151,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -2247,7 +2247,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -2371,7 +2371,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -2688,7 +2688,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2740,7 +2740,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -3114,7 +3114,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -3492,7 +3492,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -1844,7 +1844,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -1987,7 +1987,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -2083,7 +2083,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -2207,7 +2207,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -2524,7 +2524,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2576,7 +2576,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -2950,7 +2950,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -3328,7 +3328,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -25165,7 +25165,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -25291,7 +25291,7 @@ spec:
key: log.format.timestamp
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -25442,7 +25442,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -25538,7 +25538,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -25640,7 +25640,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -25931,7 +25931,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -25983,7 +25983,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -26355,7 +26355,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -26733,7 +26733,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-application-controller
ports:

16
manifests/install.yaml generated
View File

@@ -25133,7 +25133,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -25276,7 +25276,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -25372,7 +25372,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -25474,7 +25474,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -25765,7 +25765,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -25817,7 +25817,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -26189,7 +26189,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -26567,7 +26567,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -968,7 +968,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -1094,7 +1094,7 @@ spec:
key: log.format.timestamp
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1245,7 +1245,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1341,7 +1341,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -1443,7 +1443,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -1734,7 +1734,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1786,7 +1786,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -2158,7 +2158,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2536,7 +2536,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -936,7 +936,7 @@ spec:
key: applicationsetcontroller.status.max.resources.count
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -1079,7 +1079,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1175,7 +1175,7 @@ spec:
key: notificationscontroller.repo.server.plaintext
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -1277,7 +1277,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -1568,7 +1568,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1620,7 +1620,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -1992,7 +1992,7 @@ spec:
key: server.sync.replace.allowed
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2370,7 +2370,7 @@ spec:
optional: true
- name: KUBECACHEDIR
value: /tmp/kubecache
image: quay.io/argoproj/argocd:v3.1.10
image: quay.io/argoproj/argocd:v3.1.12
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -2482,7 +2482,7 @@ func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacReque
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("error getting resource: %w", err)
}
return
return obj, res, app, config, err
}
func (s *Server) getAvailableActions(resourceOverrides map[string]v1alpha1.ResourceOverride, obj *unstructured.Unstructured) ([]v1alpha1.ResourceAction, error) {

View File

@@ -117,6 +117,9 @@ export const ApplicationCreatePanel = (props: {
const debouncedOnAppChanged = debounce(props.onAppChanged, 800);
const [destinationFieldChanges, setDestinationFieldChanges] = React.useState({destFormat: 'URL', destFormatChanged: null});
const comboSwitchedFromPanel = React.useRef(false);
const currentRepoType = React.useRef(undefined);
const lastGitOrHelmUrl = React.useRef('');
const lastOciUrl = React.useRef('');
let destinationComboValue = destinationFieldChanges.destFormat;
React.useEffect(() => {
@@ -199,7 +202,7 @@ export const ApplicationCreatePanel = (props: {
const repos = reposInfo.map(info => info.repo).sort();
const repoInfo = reposInfo.find(info => info.repo === app.spec.source.repoURL);
if (repoInfo) {
normalizeAppSource(app, repoInfo.type || 'git');
normalizeAppSource(app, repoInfo.type || currentRepoType.current || 'git');
}
return (
<div className='application-create-panel'>
@@ -345,9 +348,39 @@ export const ApplicationCreatePanel = (props: {
action: () => {
if (repoType !== type) {
const updatedApp = api.getFormState().values as models.Application;
if (normalizeAppSource(updatedApp, type)) {
api.setAllValues(updatedApp);
const source = getAppDefaultSource(updatedApp);
// Save the previous URL value for later use
if (repoType === 'git' || repoType === 'helm') {
lastGitOrHelmUrl.current = source.repoURL;
} else {
lastOciUrl.current = source.repoURL;
}
currentRepoType.current = type;
switch (type) {
case 'git':
case 'oci':
if (source.hasOwnProperty('chart')) {
source.path = source.chart;
delete source.chart;
}
source.targetRevision = 'HEAD';
source.repoURL =
type === 'git'
? lastGitOrHelmUrl.current
: lastOciUrl.current === ''
? 'oci://'
: lastOciUrl.current;
break;
case 'helm':
if (source.hasOwnProperty('path')) {
source.chart = source.path;
delete source.path;
}
source.targetRevision = '';
source.repoURL = lastGitOrHelmUrl.current;
break;
}
api.setAllValues(updatedApp);
}
}
}))}

View File

@@ -37,15 +37,17 @@ export const ApplicationHydrateOperationState: React.FunctionComponent<Props> =
if (hydrateOperationState.finishedAt && hydrateOperationState.phase !== 'Hydrating') {
operationAttributes.push({title: 'FINISHED AT', value: <Timestamp date={hydrateOperationState.finishedAt} />});
}
operationAttributes.push({
title: 'DRY REVISION',
value: (
<div>
<Revision repoUrl={hydrateOperationState.sourceHydrator.drySource.repoURL} revision={hydrateOperationState.drySHA} />
</div>
)
});
if (hydrateOperationState.finishedAt) {
if (hydrateOperationState.drySHA) {
operationAttributes.push({
title: 'DRY REVISION',
value: (
<div>
<Revision repoUrl={hydrateOperationState.sourceHydrator.drySource.repoURL} revision={hydrateOperationState.drySHA} />
</div>
)
});
}
if (hydrateOperationState.finishedAt && hydrateOperationState.hydratedSHA) {
operationAttributes.push({
title: 'HYDRATED REVISION',
value: (

View File

@@ -275,13 +275,15 @@ export const ApplicationNodeInfo = (props: {
Resource not found in cluster:{' '}
{`${props?.controlled?.state?.targetState?.apiVersion}/${props?.controlled?.state?.targetState?.kind}:${props.node.name}`}
<br />
{props?.controlled?.state?.normalizedLiveState?.apiVersion && (
<span>
Please update your resource specification to use the latest Kubernetes API resources supported by the target cluster. The
recommended syntax is{' '}
{`${props.controlled.state.normalizedLiveState.apiVersion}/${props?.controlled.state.normalizedLiveState?.kind}:${props.node.name}`}
</span>
)}
{props?.controlled?.state?.normalizedLiveState?.apiVersion &&
`${props?.controlled?.state?.targetState?.apiVersion}/${props?.controlled?.state?.targetState?.kind}:${props.node.name}` !==
`${props.controlled.state.normalizedLiveState.apiVersion}/${props?.controlled.state.normalizedLiveState?.kind}:${props.node.name}` && (
<span>
Please update your resource specification to use the latest Kubernetes API resources supported by the target cluster. The
recommended syntax is{' '}
{`${props.controlled.state.normalizedLiveState.apiVersion}/${props?.controlled.state.normalizedLiveState?.kind}:${props.node.name}`}
</span>
)}
</div>
)}
</React.Fragment>

View File

@@ -494,7 +494,9 @@ export interface HydrateOperation {
finishedAt?: models.Time;
phase: HydrateOperationPhase;
message: string;
// drySHA is the sha of the DRY commit being hydrated. This will be empty if the operation is not successful.
drySHA: string;
// hydratedSHA is the sha of the hydrated commit. This will be empty if the operation is not successful.
hydratedSHA: string;
sourceHydrator: SourceHydrator;
}

View File

@@ -6437,9 +6437,9 @@ locate-path@^6.0.0:
p-locate "^5.0.0"
lodash-es@^4.17.21, lodash-es@^4.2.1:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
version "4.17.23"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0"
integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==
lodash.memoize@4.x:
version "4.1.2"
@@ -6452,9 +6452,9 @@ lodash.merge@^4.6.2:
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.2.1, lodash@^4.6.1:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
version "4.17.23"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"

View File

@@ -97,23 +97,41 @@ func CheckOutOfBoundsSymlinks(basePath string) error {
})
}
// GetAppRefreshPaths returns the list of paths that should trigger a refresh for an application
func GetAppRefreshPaths(app *v1alpha1.Application) []string {
// GetSourceRefreshPaths returns the list of paths that should trigger a refresh for an application.
// The source parameter influences the returned refresh paths:
// - if source hydrator configured AND source is syncSource: use sync source path (ignores annotation)
// - if source hydrator configured AND source is drySource WITH annotation: use annotation paths with drySource base
// - if source hydrator not configured: use annotation paths with source base, or empty if no annotation
func GetSourceRefreshPaths(app *v1alpha1.Application, source v1alpha1.ApplicationSource) []string {
annotationPaths, hasAnnotation := app.Annotations[v1alpha1.AnnotationKeyManifestGeneratePaths]
if app.Spec.SourceHydrator != nil {
syncSource := app.Spec.SourceHydrator.GetSyncSource()
// if source is syncSource use the source path
if (source).Equals(&syncSource) {
return []string{source.Path}
}
}
var paths []string
if val, ok := app.Annotations[v1alpha1.AnnotationKeyManifestGeneratePaths]; ok && val != "" {
for _, item := range strings.Split(val, ";") {
if hasAnnotation && annotationPaths != "" {
for _, item := range strings.Split(annotationPaths, ";") {
// skip empty paths
if item == "" {
continue
}
// if absolute path, add as is
if filepath.IsAbs(item) {
paths = append(paths, item[1:])
} else {
for _, source := range app.Spec.GetSources() {
paths = append(paths, filepath.Clean(filepath.Join(source.Path, item)))
}
continue
}
// add the path relative to the source path
paths = append(paths, filepath.Clean(filepath.Join(source.Path, item)))
}
}
return paths
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
fileutil "github.com/argoproj/argo-cd/v3/test/fixture/path"
@@ -100,96 +101,38 @@ func TestAbsSymlink(t *testing.T) {
assert.Equal(t, "abslink", oobError.File)
}
func getApp(annotation string, sourcePath string) *v1alpha1.Application {
return &v1alpha1.Application{
func getApp(annotation *string, sourcePath *string) *v1alpha1.Application {
app := &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1alpha1.AnnotationKeyManifestGeneratePaths: annotation,
},
},
Spec: v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{
Path: sourcePath,
},
Name: "test-app",
},
}
if annotation != nil {
app.Annotations = make(map[string]string)
app.Annotations[v1alpha1.AnnotationKeyManifestGeneratePaths] = *annotation
}
if sourcePath != nil {
app.Spec.Source = &v1alpha1.ApplicationSource{
Path: *sourcePath,
}
}
return app
}
func getMultiSourceApp(annotation string, paths ...string) *v1alpha1.Application {
var sources v1alpha1.ApplicationSources
for _, path := range paths {
sources = append(sources, v1alpha1.ApplicationSource{Path: path})
}
return &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
v1alpha1.AnnotationKeyManifestGeneratePaths: annotation,
},
func getSourceHydratorApp(annotation *string, drySourcePath string, syncSourcePath string) *v1alpha1.Application {
app := getApp(annotation, nil)
app.Spec.SourceHydrator = &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
Path: drySourcePath,
},
Spec: v1alpha1.ApplicationSpec{
Sources: sources,
SyncSource: v1alpha1.SyncSource{
Path: syncSourcePath,
},
}
}
func Test_AppFilesHaveChanged(t *testing.T) {
t.Parallel()
tests := []struct {
name string
app *v1alpha1.Application
files []string
changeExpected bool
}{
{"default no path", &v1alpha1.Application{}, []string{"README.md"}, true},
{"no files changed", getApp(".", "source/path"), []string{}, true},
{"relative path - matching", getApp(".", "source/path"), []string{"source/path/my-deployment.yaml"}, true},
{"relative path, multi source - matching #1", getMultiSourceApp(".", "source/path", "other/path"), []string{"source/path/my-deployment.yaml"}, true},
{"relative path, multi source - matching #2", getMultiSourceApp(".", "other/path", "source/path"), []string{"source/path/my-deployment.yaml"}, true},
{"relative path - not matching", getApp(".", "source/path"), []string{"README.md"}, false},
{"relative path, multi source - not matching", getMultiSourceApp(".", "other/path", "unrelated/path"), []string{"README.md"}, false},
{"absolute path - matching", getApp("/source/path", "source/path"), []string{"source/path/my-deployment.yaml"}, true},
{"absolute path, multi source - matching #1", getMultiSourceApp("/source/path", "source/path", "other/path"), []string{"source/path/my-deployment.yaml"}, true},
{"absolute path, multi source - matching #2", getMultiSourceApp("/source/path", "other/path", "source/path"), []string{"source/path/my-deployment.yaml"}, true},
{"absolute path - not matching", getApp("/source/path1", "source/path"), []string{"source/path/my-deployment.yaml"}, false},
{"absolute path, multi source - not matching", getMultiSourceApp("/source/path1", "other/path", "source/path"), []string{"source/path/my-deployment.yaml"}, false},
{"glob path * - matching", getApp("/source/**/my-deployment.yaml", "source/path"), []string{"source/path/my-deployment.yaml"}, true},
{"glob path * - not matching", getApp("/source/**/my-service.yaml", "source/path"), []string{"source/path/my-deployment.yaml"}, false},
{"glob path ? - matching", getApp("/source/path/my-deployment-?.yaml", "source/path"), []string{"source/path/my-deployment-0.yaml"}, true},
{"glob path ? - not matching", getApp("/source/path/my-deployment-?.yaml", "source/path"), []string{"source/path/my-deployment.yaml"}, false},
{"glob path char range - matching", getApp("/source/path[0-9]/my-deployment.yaml", "source/path"), []string{"source/path1/my-deployment.yaml"}, true},
{"glob path char range - not matching", getApp("/source/path[0-9]/my-deployment.yaml", "source/path"), []string{"source/path/my-deployment.yaml"}, false},
{"mixed glob path - matching", getApp("/source/path[0-9]/my-*.yaml", "source/path"), []string{"source/path1/my-deployment.yaml"}, true},
{"mixed glob path - not matching", getApp("/source/path[0-9]/my-*.yaml", "source/path"), []string{"README.md"}, false},
{"two relative paths - matching", getApp(".;../shared", "my-app"), []string{"shared/my-deployment.yaml"}, true},
{"two relative paths, multi source - matching #1", getMultiSourceApp(".;../shared", "my-app", "other/path"), []string{"shared/my-deployment.yaml"}, true},
{"two relative paths, multi source - matching #2", getMultiSourceApp(".;../shared", "my-app", "other/path"), []string{"shared/my-deployment.yaml"}, true},
{"two relative paths - not matching", getApp(".;../shared", "my-app"), []string{"README.md"}, false},
{"two relative paths, multi source - not matching", getMultiSourceApp(".;../shared", "my-app", "other/path"), []string{"README.md"}, false},
{"file relative path - matching", getApp("./my-deployment.yaml", "source/path"), []string{"source/path/my-deployment.yaml"}, true},
{"file relative path, multi source - matching #1", getMultiSourceApp("./my-deployment.yaml", "source/path", "other/path"), []string{"source/path/my-deployment.yaml"}, true},
{"file relative path, multi source - matching #2", getMultiSourceApp("./my-deployment.yaml", "other/path", "source/path"), []string{"source/path/my-deployment.yaml"}, true},
{"file relative path - not matching", getApp("./my-deployment.yaml", "source/path"), []string{"README.md"}, false},
{"file relative path, multi source - not matching", getMultiSourceApp("./my-deployment.yaml", "source/path", "other/path"), []string{"README.md"}, false},
{"file absolute path - matching", getApp("/source/path/my-deployment.yaml", "source/path"), []string{"source/path/my-deployment.yaml"}, true},
{"file absolute path, multi source - matching #1", getMultiSourceApp("/source/path/my-deployment.yaml", "source/path", "other/path"), []string{"source/path/my-deployment.yaml"}, true},
{"file absolute path, multi source - matching #2", getMultiSourceApp("/source/path/my-deployment.yaml", "other/path", "source/path"), []string{"source/path/my-deployment.yaml"}, true},
{"file absolute path - not matching", getApp("/source/path1/README.md", "source/path"), []string{"source/path/my-deployment.yaml"}, false},
{"file absolute path, multi source - not matching", getMultiSourceApp("/source/path1/README.md", "source/path", "other/path"), []string{"source/path/my-deployment.yaml"}, false},
{"file two relative paths - matching", getApp("./README.md;../shared/my-deployment.yaml", "my-app"), []string{"shared/my-deployment.yaml"}, true},
{"file two relative paths, multi source - matching", getMultiSourceApp("./README.md;../shared/my-deployment.yaml", "my-app", "other-path"), []string{"shared/my-deployment.yaml"}, true},
{"file two relative paths - not matching", getApp(".README.md;../shared/my-deployment.yaml", "my-app"), []string{"kustomization.yaml"}, false},
{"file two relative paths, multi source - not matching", getMultiSourceApp(".README.md;../shared/my-deployment.yaml", "my-app", "other-path"), []string{"kustomization.yaml"}, false},
{"changed file absolute path - matching", getApp(".", "source/path"), []string{"/source/path/my-deployment.yaml"}, true},
}
for _, tt := range tests {
ttc := tt
t.Run(ttc.name, func(t *testing.T) {
t.Parallel()
refreshPaths := GetAppRefreshPaths(ttc.app)
assert.Equal(t, ttc.changeExpected, AppFilesHaveChanged(refreshPaths, ttc.files), "AppFilesHaveChanged()")
})
}
return app
}
func Test_GetAppRefreshPaths(t *testing.T) {
@@ -198,23 +141,64 @@ func Test_GetAppRefreshPaths(t *testing.T) {
tests := []struct {
name string
app *v1alpha1.Application
source v1alpha1.ApplicationSource
expectedPaths []string
}{
{"default no path", &v1alpha1.Application{}, []string{}},
{"relative path", getApp(".", "source/path"), []string{"source/path"}},
{"absolute path - multi source", getMultiSourceApp("/source/path", "source/path", "other/path"), []string{"source/path"}},
{"two relative paths ", getApp(".;../shared", "my-app"), []string{"my-app", "shared"}},
{"file relative path", getApp("./my-deployment.yaml", "source/path"), []string{"source/path/my-deployment.yaml"}},
{"file absolute path", getApp("/source/path/my-deployment.yaml", "source/path"), []string{"source/path/my-deployment.yaml"}},
{"file two relative paths", getApp("./README.md;../shared/my-deployment.yaml", "my-app"), []string{"my-app/README.md", "shared/my-deployment.yaml"}},
{"glob path", getApp("/source/*/my-deployment.yaml", "source/path"), []string{"source/*/my-deployment.yaml"}},
{"empty path", getApp(".;", "source/path"), []string{"source/path"}},
{
name: "single source without annotation",
app: getApp(nil, ptr.To("source/path")),
source: v1alpha1.ApplicationSource{Path: "source/path"},
expectedPaths: []string{},
},
{
name: "single source with annotation",
app: getApp(ptr.To(".;dev/deploy;other/path"), ptr.To("source/path")),
source: v1alpha1.ApplicationSource{Path: "source/path"},
expectedPaths: []string{"source/path", "source/path/dev/deploy", "source/path/other/path"},
},
{
name: "single source with empty annotation",
app: getApp(ptr.To(".;;"), ptr.To("source/path")),
source: v1alpha1.ApplicationSource{Path: "source/path"},
expectedPaths: []string{"source/path"},
},
{
name: "single source with absolute path annotation",
app: getApp(ptr.To("/fullpath/deploy;other/path"), ptr.To("source/path")),
source: v1alpha1.ApplicationSource{Path: "source/path"},
expectedPaths: []string{"fullpath/deploy", "source/path/other/path"},
},
{
name: "source hydrator sync source without annotation",
app: getSourceHydratorApp(nil, "dry/path", "sync/path"),
source: v1alpha1.ApplicationSource{Path: "sync/path"},
expectedPaths: []string{"sync/path"},
},
{
name: "source hydrator dry source without annotation",
app: getSourceHydratorApp(nil, "dry/path", "sync/path"),
source: v1alpha1.ApplicationSource{Path: "dry/path"},
expectedPaths: []string{},
},
{
name: "source hydrator sync source with annotation",
app: getSourceHydratorApp(ptr.To("deploy"), "dry/path", "sync/path"),
source: v1alpha1.ApplicationSource{Path: "sync/path"},
expectedPaths: []string{"sync/path"},
},
{
name: "source hydrator dry source with annotation",
app: getSourceHydratorApp(ptr.To("deploy"), "dry/path", "sync/path"),
source: v1alpha1.ApplicationSource{Path: "dry/path"},
expectedPaths: []string{"dry/path/deploy"},
},
}
for _, tt := range tests {
ttc := tt
t.Run(ttc.name, func(t *testing.T) {
t.Parallel()
assert.ElementsMatch(t, ttc.expectedPaths, GetAppRefreshPaths(ttc.app), "GetAppRefreshPath()")
assert.ElementsMatch(t, ttc.expectedPaths, GetSourceRefreshPaths(ttc.app, ttc.source), "GetAppRefreshPath()")
})
}
}

View File

@@ -1159,7 +1159,7 @@ func parseName(qualifiedName string, defaultNs string, delim string) (name strin
namespace = defaultNs
name = t[0]
}
return
return name, namespace
}
// ParseAppNamespacedName parses a namespaced name in the format namespace/name

View File

@@ -670,7 +670,7 @@ func (m *nativeGitClient) LsRemote(revision string) (res string, err error) {
for attempt := 0; attempt < maxAttemptsCount; attempt++ {
res, err = m.lsRemote(revision)
if err == nil {
return
return res, nil
} else if apierrors.IsInternalError(err) || apierrors.IsTimeout(err) || apierrors.IsServerTimeout(err) ||
apierrors.IsTooManyRequests(err) || utilnet.IsProbableEOF(err) || utilnet.IsConnectionReset(err) {
// Formula: timeToWait = duration * factor^retry_number
@@ -683,7 +683,7 @@ func (m *nativeGitClient) LsRemote(revision string) (res string, err error) {
time.Sleep(time.Duration(timeToWait))
}
}
return
return res, err
}
func getGitTags(refs []*plumbing.Reference) []string {

View File

@@ -3,6 +3,7 @@ package healthz
import (
"fmt"
"net/http"
"time"
log "github.com/sirupsen/logrus"
)
@@ -11,9 +12,13 @@ import (
// ServeHealthCheck relies on the provided function to return an error if unhealthy and nil otherwise.
func ServeHealthCheck(mux *http.ServeMux, f func(r *http.Request) error) {
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
startTs := time.Now()
if err := f(r); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
log.Errorln(w, err)
log.WithFields(log.Fields{
"duration": time.Since(startTs),
"component": "healthcheck",
}).WithError(err).Error("Error serving health check request")
} else {
fmt.Fprintln(w, "ok")
}

View File

@@ -5,16 +5,22 @@ import (
"net"
"net/http"
"testing"
"time"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHealthCheck(t *testing.T) {
sentinel := false
lc := &net.ListenConfig{}
ctx := t.Context()
svcErrMsg := "This is a dummy error"
serve := func(c chan<- string) {
// listen on first available dynamic (unprivileged) port
listener, err := net.Listen("tcp", ":0")
listener, err := lc.Listen(ctx, "tcp", ":0")
if err != nil {
panic(err)
}
@@ -25,7 +31,7 @@ func TestHealthCheck(t *testing.T) {
mux := http.NewServeMux()
ServeHealthCheck(mux, func(_ *http.Request) error {
if sentinel {
return errors.New("This is a dummy error")
return errors.New(svcErrMsg)
}
return nil
})
@@ -47,7 +53,23 @@ func TestHealthCheck(t *testing.T) {
require.Equalf(t, http.StatusOK, resp.StatusCode, "Was expecting status code 200 from health check, but got %d instead", resp.StatusCode)
sentinel = true
hook := test.NewGlobal()
resp, _ = http.Get(server + "/healthz")
require.Equalf(t, http.StatusServiceUnavailable, resp.StatusCode, "Was expecting status code 503 from health check, but got %d instead", resp.StatusCode)
assert.NotEmpty(t, hook.Entries, "Was expecting at least one log entry from health check, but got none")
expectedMsg := "Error serving health check request"
var foundEntry log.Entry
for _, entry := range hook.Entries {
if entry.Level == log.ErrorLevel &&
entry.Message == expectedMsg {
foundEntry = entry
break
}
}
require.NotEmpty(t, foundEntry, "Expected an error message '%s', but it was't found", expectedMsg)
actualErr, ok := foundEntry.Data["error"].(error)
require.True(t, ok, "Expected 'error' field to contain an error, but it doesn't")
assert.Equal(t, svcErrMsg, actualErr.Error(), "expected original error message '"+svcErrMsg+"', but got '"+actualErr.Error()+"'")
assert.Greater(t, foundEntry.Data["duration"].(time.Duration), time.Duration(0))
}

View File

@@ -19,5 +19,5 @@ func (c composableFS) Open(name string) (f fs.File, err error) {
break
}
}
return
return f, err
}

View File

@@ -109,7 +109,8 @@ func TestFailExternalLibCall(t *testing.T) {
vm := VM{}
_, err := vm.ExecuteHealthLua(testObj, osLuaScript)
require.Error(t, err)
assert.IsType(t, &lua.ApiError{}, err)
var apiErr *lua.ApiError
assert.ErrorAs(t, err, &apiErr)
}
const returnInt = `return 1`
@@ -183,7 +184,8 @@ func TestHandleInfiniteLoop(t *testing.T) {
testObj := StrToUnstructured(objJSON)
vm := VM{}
_, err := vm.ExecuteHealthLua(testObj, infiniteLoop)
assert.IsType(t, &lua.ApiError{}, err)
var apiErr *lua.ApiError
assert.ErrorAs(t, err, &apiErr)
}
func TestGetHealthScriptWithOverride(t *testing.T) {
@@ -954,7 +956,8 @@ return hs`
testObj := StrToUnstructured(testSA)
overrides := getHealthOverride(false)
status, err := overrides.GetResourceHealth(testObj)
assert.IsType(t, &lua.ApiError{}, err)
var apiErr *lua.ApiError
require.ErrorAs(t, err, &apiErr)
expectedErr := "<string>:4: attempt to index a non-table object(nil) with key 'find'"
require.EqualError(t, err, expectedErr)
assert.Nil(t, status)

View File

@@ -70,7 +70,7 @@ func HashPassword(password string) (string, error) {
// VerifyPassword verifies an entered password against a hashed password and returns whether the hash is "stale" (i.e., was verified using the FIRST preferred hasher above).
func VerifyPassword(password, hashedPassword string) (valid, stale bool) {
valid, stale = verifyPasswordWithHashers(password, hashedPassword, preferredHashers)
return
return valid, stale
}
// HashPassword creates a one-way digest ("hash") of a password. In the case of Bcrypt, a pseudorandom salt is included automatically by the underlying library.

View File

@@ -330,23 +330,23 @@ func (a *ArgoCDWebhookHandler) HandleEvent(payload any) {
appIf := a.appsLister.Applications(nsFilter)
apps, err := appIf.List(labels.Everything())
if err != nil {
log.Warnf("Failed to list applications: %v", err)
log.Errorf("Failed to list applications: %v", err)
return
}
installationID, err := a.settingsSrc.GetInstallationID()
if err != nil {
log.Warnf("Failed to get installation ID: %v", err)
log.Errorf("Failed to get installation ID: %v", err)
return
}
trackingMethod, err := a.settingsSrc.GetTrackingMethod()
if err != nil {
log.Warnf("Failed to get trackingMethod: %v", err)
log.Errorf("Failed to get trackingMethod: %v", err)
return
}
appInstanceLabelKey, err := a.settingsSrc.GetAppInstanceLabelKey()
if err != nil {
log.Warnf("Failed to get appInstanceLabelKey: %v", err)
log.Errorf("Failed to get appInstanceLabelKey: %v", err)
return
}
@@ -362,41 +362,47 @@ func (a *ArgoCDWebhookHandler) HandleEvent(payload any) {
for _, webURL := range webURLs {
repoRegexp, err := GetWebURLRegex(webURL)
if err != nil {
log.Warnf("Failed to get repoRegexp: %s", err)
log.Errorf("Failed to get repoRegexp: %s", err)
continue
}
// 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
sources := app.Spec.GetSources()
if app.Spec.SourceHydrator != nil {
drySource := app.Spec.SourceHydrator.GetDrySource()
if sourceRevisionHasChanged(drySource, revision, touchedHead) && sourceUsesURL(drySource, webURL, repoRegexp) {
refreshPaths := path.GetAppRefreshPaths(&app)
if path.AppFilesHaveChanged(refreshPaths, changedFiles) {
namespacedAppInterface := a.appClientset.ArgoprojV1alpha1().Applications(app.Namespace)
log.Infof("webhook trigger refresh app to hydrate '%s'", app.Name)
_, err = argo.RefreshApp(namespacedAppInterface, app.Name, v1alpha1.RefreshTypeNormal, true)
if err != nil {
log.Warnf("Failed to hydrate app '%s' for controller reprocessing: %v", app.Name, err)
continue
}
}
}
// we already have sync source, so add dry source if source hydrator is configured
sources = append(sources, app.Spec.SourceHydrator.GetDrySource())
}
for _, source := range app.Spec.GetSources() {
// iterate over all sources and check if any files specified in refresh paths have changed
for _, source := range sources {
if sourceRevisionHasChanged(source, revision, touchedHead) && sourceUsesURL(source, webURL, repoRegexp) {
refreshPaths := path.GetAppRefreshPaths(&app)
refreshPaths := path.GetSourceRefreshPaths(&app, source)
if path.AppFilesHaveChanged(refreshPaths, changedFiles) {
namespacedAppInterface := a.appClientset.ArgoprojV1alpha1().Applications(app.Namespace)
_, err = argo.RefreshApp(namespacedAppInterface, app.Name, v1alpha1.RefreshTypeNormal, true)
if err != nil {
log.Warnf("Failed to refresh app '%s' for controller reprocessing: %v", app.Name, err)
continue
hydrate := false
if app.Spec.SourceHydrator != nil {
drySource := app.Spec.SourceHydrator.GetDrySource()
if (&source).Equals(&drySource) {
hydrate = true
}
}
// No need to refresh multiple times if multiple sources match.
break
// refresh paths have changed, so we need to refresh the app
log.Infof("refreshing app '%s' from webhook", app.Name)
if hydrate {
// log if we need to hydrate the app
log.Infof("webhook trigger refresh app to hydrate '%s'", app.Name)
}
namespacedAppInterface := a.appClientset.ArgoprojV1alpha1().Applications(app.Namespace)
if _, err := argo.RefreshApp(namespacedAppInterface, app.Name, v1alpha1.RefreshTypeNormal, hydrate); err != nil {
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 != "" {
if err := a.storePreviouslyCachedManifests(&app, change, trackingMethod, appInstanceLabelKey, installationID); err != nil {
log.Warnf("Failed to store cached manifests of previous revision for app '%s': %v", app.Name, err)
// 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)
}
}
}
@@ -449,7 +455,7 @@ func getURLRegex(originalURL string, regexpFormat string) (*regexp.Regexp, error
return repoRegexp, nil
}
func (a *ArgoCDWebhookHandler) storePreviouslyCachedManifests(app *v1alpha1.Application, change changeInfo, trackingMethod string, appInstanceLabelKey string, installationID string) error {
func (a *ArgoCDWebhookHandler) storePreviouslyCachedManifests(app *v1alpha1.Application, change changeInfo, trackingMethod string, appInstanceLabelKey string, installationID string, source v1alpha1.ApplicationSource) error {
destCluster, err := argo.GetDestinationCluster(context.Background(), app.Spec.Destination, a.db)
if err != nil {
return fmt.Errorf("error validating destination: %w", err)
@@ -472,7 +478,7 @@ func (a *ArgoCDWebhookHandler) storePreviouslyCachedManifests(app *v1alpha1.Appl
if err != nil {
return fmt.Errorf("error getting ref sources: %w", err)
}
source := app.Spec.GetSource()
cache.LogDebugManifestCacheKeyFields("moving manifests cache", "webhook app revision changed", change.shaBefore, &source, refSources, &clusterInfo, app.Spec.Destination.Namespace, trackingMethod, appInstanceLabelKey, app.Name, nil)
if err := a.repoCache.SetNewRevisionManifests(change.shaAfter, change.shaBefore, &source, refSources, &clusterInfo, app.Spec.Destination.Namespace, trackingMethod, appInstanceLabelKey, app.Name, nil, installationID); err != nil {

View File

@@ -44,6 +44,7 @@ import (
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/fake"
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
"github.com/argoproj/argo-cd/v3/reposerver/cache"
cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
"github.com/argoproj/argo-cd/v3/util/settings"
@@ -182,64 +183,6 @@ func TestAzureDevOpsCommitEvent(t *testing.T) {
hook.Reset()
}
// TestGitHubCommitEvent_MultiSource_Refresh makes sure that a webhook will refresh a multi-source app when at least
// one source matches.
func TestGitHubCommitEvent_MultiSource_Refresh(t *testing.T) {
hook := test.NewGlobal()
var patched bool
reaction := func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
patchAction := action.(kubetesting.PatchAction)
assert.Equal(t, "app-to-refresh", patchAction.GetName())
patched = true
return true, nil, nil
}
h := NewMockHandler(&reactorDef{"patch", "applications", reaction}, []string{}, &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "app-to-refresh",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSpec{
Sources: v1alpha1.ApplicationSources{
{
RepoURL: "https://github.com/some/unrelated-repo",
Path: ".",
},
{
RepoURL: "https://github.com/jessesuen/test-repo",
Path: ".",
},
},
},
}, &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "app-to-ignore",
},
Spec: v1alpha1.ApplicationSpec{
Sources: v1alpha1.ApplicationSources{
{
RepoURL: "https://github.com/some/unrelated-repo",
Path: ".",
},
},
},
},
)
req := httptest.NewRequest(http.MethodPost, "/api/webhook", http.NoBody)
req.Header.Set("X-GitHub-Event", "push")
eventJSON, err := os.ReadFile("testdata/github-commit-event.json")
require.NoError(t, err)
req.Body = io.NopCloser(bytes.NewReader(eventJSON))
w := httptest.NewRecorder()
h.Handler(w, req)
close(h.queue)
h.Wait()
assert.Equal(t, http.StatusOK, w.Code)
expectedLogResult := "Requested app 'app-to-refresh' refresh"
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
assert.True(t, patched)
hook.Reset()
}
// TestGitHubCommitEvent_AppsInOtherNamespaces makes sure that webhooks properly find apps in the configured set of
// allowed namespaces when Apps are allowed in any namespace
func TestGitHubCommitEvent_AppsInOtherNamespaces(t *testing.T) {
@@ -338,72 +281,6 @@ func TestGitHubCommitEvent_AppsInOtherNamespaces(t *testing.T) {
hook.Reset()
}
// TestGitHubCommitEvent_Hydrate makes sure that a webhook will hydrate an app when dry source changed.
func TestGitHubCommitEvent_Hydrate(t *testing.T) {
hook := test.NewGlobal()
var patched bool
reaction := func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
patchAction := action.(kubetesting.PatchAction)
assert.Equal(t, "app-to-hydrate", patchAction.GetName())
patched = true
return true, nil, nil
}
h := NewMockHandler(&reactorDef{"patch", "applications", reaction}, []string{}, &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "app-to-hydrate",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://github.com/jessesuen/test-repo",
TargetRevision: "HEAD",
Path: ".",
},
SyncSource: v1alpha1.SyncSource{
TargetBranch: "environments/dev",
Path: ".",
},
HydrateTo: nil,
},
},
}, &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "app-to-ignore",
},
Spec: v1alpha1.ApplicationSpec{
Sources: v1alpha1.ApplicationSources{
{
RepoURL: "https://github.com/some/unrelated-repo",
Path: ".",
},
},
},
},
)
req := httptest.NewRequest(http.MethodPost, "/api/webhook", http.NoBody)
req.Header.Set("X-GitHub-Event", "push")
eventJSON, err := os.ReadFile("testdata/github-commit-event.json")
require.NoError(t, err)
req.Body = io.NopCloser(bytes.NewReader(eventJSON))
w := httptest.NewRecorder()
h.Handler(w, req)
close(h.queue)
h.Wait()
assert.Equal(t, http.StatusOK, w.Code)
assert.True(t, patched)
logMessages := make([]string, 0, len(hook.Entries))
for _, entry := range hook.Entries {
logMessages = append(logMessages, entry.Message)
}
assert.Contains(t, logMessages, "webhook trigger refresh app to hydrate 'app-to-hydrate'")
assert.NotContains(t, logMessages, "webhook trigger refresh app to hydrate 'app-to-ignore'")
hook.Reset()
}
func TestGitHubTagEvent(t *testing.T) {
hook := test.NewGlobal()
h := NewMockHandler(nil, []string{})
@@ -646,7 +523,8 @@ func Test_affectedRevisionInfo_appRevisionHasChanged(t *testing.T) {
// The payload's "push.changes[0].new.name" member seems to only have the branch name (based on the example payload).
// https://support.atlassian.com/bitbucket-cloud/docs/event-payloads/#EventPayloads-Push
var pl bitbucket.RepoPushPayload
_ = json.Unmarshal([]byte(fmt.Sprintf(`{"push":{"changes":[{"new":{"name":%q}}]}}`, branchName)), &pl)
err := json.Unmarshal([]byte(fmt.Sprintf(`{"push":{"changes":[{"new":{"name":%q}}]}}`, branchName)), &pl)
require.NoError(t, err)
return pl
}
@@ -878,6 +756,463 @@ func TestGitHubCommitEventMaxPayloadSize(t *testing.T) {
hook.Reset()
}
func TestHandleEvent(t *testing.T) {
t.Parallel()
tests := []struct {
name string
app *v1alpha1.Application
changedFile string // file that was changed in the webhook payload
hasRefresh bool // application has refresh annotation applied
hasHydrate bool // application has hydrate annotation applied
updateCache bool // cache should be updated with the new revision
}{
{
name: "single source without annotation - always refreshes",
app: &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSpec{
Sources: v1alpha1.ApplicationSources{
{
RepoURL: "https://github.com/jessesuen/test-repo",
Path: "source/path",
TargetRevision: "HEAD",
},
},
},
},
changedFile: "source/path/app.yaml",
hasRefresh: true,
hasHydrate: false,
updateCache: false,
},
{
name: "single source with annotation - matching file triggers refresh",
app: &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
Annotations: map[string]string{
"argocd.argoproj.io/manifest-generate-paths": "deploy",
},
},
Spec: v1alpha1.ApplicationSpec{
Sources: v1alpha1.ApplicationSources{
{
RepoURL: "https://github.com/jessesuen/test-repo",
Path: "source/path",
TargetRevision: "HEAD",
},
},
},
},
changedFile: "source/path/deploy/app.yaml",
hasRefresh: true,
hasHydrate: false,
updateCache: false,
},
{
name: "single source with annotation - non-matching file updates cache",
app: &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
Annotations: map[string]string{
"argocd.argoproj.io/manifest-generate-paths": "manifests",
},
},
Spec: v1alpha1.ApplicationSpec{
Sources: v1alpha1.ApplicationSources{
{
RepoURL: "https://github.com/jessesuen/test-repo",
Path: "source/path",
TargetRevision: "HEAD",
},
},
},
},
changedFile: "source/path/other/app.yaml",
hasRefresh: false,
hasHydrate: false,
updateCache: true,
},
{
name: "single source with multiple paths annotation - matching subpath triggers refresh",
app: &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
Annotations: map[string]string{
"argocd.argoproj.io/manifest-generate-paths": "manifests;dev/deploy;other/path",
},
},
Spec: v1alpha1.ApplicationSpec{
Sources: v1alpha1.ApplicationSources{
{
RepoURL: "https://github.com/jessesuen/test-repo",
Path: "source/path",
TargetRevision: "HEAD",
},
},
},
},
changedFile: "source/path/dev/deploy/app.yaml",
hasRefresh: true,
hasHydrate: false,
updateCache: false,
},
{
name: "multi-source without annotation - always refreshes",
app: &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSpec{
Sources: v1alpha1.ApplicationSources{
{
RepoURL: "https://github.com/jessesuen/test-repo",
Path: "helm-charts",
TargetRevision: "HEAD",
},
{
RepoURL: "https://github.com/jessesuen/test-repo",
Path: "ksapps",
TargetRevision: "HEAD",
},
},
},
},
changedFile: "ksapps/app.yaml",
hasRefresh: true,
hasHydrate: false,
updateCache: false,
},
{
name: "multi-source with annotation - matching file triggers refresh",
app: &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
Annotations: map[string]string{
"argocd.argoproj.io/manifest-generate-paths": "components",
},
},
Spec: v1alpha1.ApplicationSpec{
Sources: v1alpha1.ApplicationSources{
{
RepoURL: "https://github.com/jessesuen/test-repo",
Path: "helm-charts",
TargetRevision: "HEAD",
},
{
RepoURL: "https://github.com/jessesuen/test-repo",
Path: "ksapps",
TargetRevision: "HEAD",
},
},
},
},
changedFile: "ksapps/components/app.yaml",
hasRefresh: true,
hasHydrate: false,
updateCache: false,
},
{
name: "source hydrator sync source without annotation - refreshes when sync path matches",
app: &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://github.com/jessesuen/test-repo",
TargetRevision: "HEAD",
Path: "dry/path",
},
SyncSource: v1alpha1.SyncSource{
TargetBranch: "master",
Path: "sync/path",
},
},
},
},
changedFile: "sync/path/app.yaml",
hasRefresh: true,
hasHydrate: false,
updateCache: false,
},
{
name: "source hydrator dry source without annotation - always refreshes and hydrates",
app: &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://github.com/jessesuen/test-repo",
TargetRevision: "HEAD",
Path: "dry/path",
},
SyncSource: v1alpha1.SyncSource{
TargetBranch: "master",
Path: "sync/path",
},
},
},
},
changedFile: "other/path/app.yaml",
hasRefresh: true,
hasHydrate: true,
updateCache: false,
},
{
name: "source hydrator sync source with annotation - refresh only",
app: &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
Annotations: map[string]string{
"argocd.argoproj.io/manifest-generate-paths": "deploy",
},
},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://github.com/jessesuen/test-repo",
TargetRevision: "HEAD",
Path: "dry/path",
},
SyncSource: v1alpha1.SyncSource{
TargetBranch: "master",
Path: "sync/path",
},
},
},
},
changedFile: "sync/path/deploy/app.yaml",
hasRefresh: true,
hasHydrate: false,
updateCache: false,
},
{
name: "source hydrator dry source with annotation - refresh and hydrate",
app: &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
Annotations: map[string]string{
"argocd.argoproj.io/manifest-generate-paths": "deploy",
},
},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://github.com/jessesuen/test-repo",
TargetRevision: "HEAD",
Path: "dry/path",
},
SyncSource: v1alpha1.SyncSource{
TargetBranch: "master",
Path: "sync/path",
},
},
},
},
changedFile: "dry/path/deploy/app.yaml",
hasRefresh: true,
hasHydrate: true,
updateCache: false,
},
{
name: "source hydrator dry source with annotation - non-matching file updates cache",
app: &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
Annotations: map[string]string{
"argocd.argoproj.io/manifest-generate-paths": "deploy",
},
},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://github.com/jessesuen/test-repo",
TargetRevision: "HEAD",
Path: "dry/path",
},
SyncSource: v1alpha1.SyncSource{
TargetBranch: "master",
Path: "sync/path",
},
},
},
},
changedFile: "dry/path/other/app.yaml",
hasRefresh: false,
hasHydrate: false,
updateCache: true,
},
}
for _, tt := range tests {
ttc := tt
t.Run(ttc.name, func(t *testing.T) {
t.Parallel()
var patchData []byte
var patched bool
reaction := func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
if action.GetVerb() == "patch" {
patchAction := action.(kubetesting.PatchAction)
patchData = patchAction.GetPatch()
patched = true
}
return true, nil, nil
}
// Setup cache
inMemoryCache := cacheutil.NewInMemoryCache(1 * time.Hour)
cacheClient := cacheutil.NewCache(inMemoryCache)
repoCache := cache.NewCache(
cacheClient,
1*time.Minute,
1*time.Minute,
10*time.Second,
)
// Pre-populate cache with beforeSHA if we're testing cache updates
if ttc.updateCache {
var source *v1alpha1.ApplicationSource
if ttc.app.Spec.SourceHydrator != nil {
drySource := ttc.app.Spec.SourceHydrator.GetDrySource()
source = &drySource
} else if len(ttc.app.Spec.Sources) > 0 {
source = &ttc.app.Spec.Sources[0]
}
if source != nil {
setupTestCache(t, repoCache, ttc.app.Name, source, []string{"test-manifest"})
}
}
// Setup server cache with cluster info
serverCache := servercache.NewCache(appstate.NewCache(cacheClient, time.Minute), time.Minute, time.Minute)
mockDB := &mocks.ArgoDB{}
// Set destination if not present (required for cache updates)
if ttc.app.Spec.Destination.Server == "" {
ttc.app.Spec.Destination.Server = testClusterURL
}
mockDB.EXPECT().GetCluster(mock.Anything, testClusterURL).Return(&v1alpha1.Cluster{
Server: testClusterURL,
Info: v1alpha1.ClusterInfo{
ServerVersion: "1.28.0",
ConnectionState: v1alpha1.ConnectionState{Status: v1alpha1.ConnectionStatusSuccessful},
APIVersions: []string{},
},
}, nil).Maybe()
err := serverCache.SetClusterInfo(testClusterURL, &v1alpha1.ClusterInfo{
ServerVersion: "1.28.0",
ConnectionState: v1alpha1.ConnectionState{Status: v1alpha1.ConnectionStatusSuccessful},
APIVersions: []string{},
})
require.NoError(t, err)
// Create handler with reaction
appClientset := appclientset.NewSimpleClientset(ttc.app)
defaultReactor := appClientset.ReactionChain[0]
appClientset.ReactionChain = nil
appClientset.AddReactor("list", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
return defaultReactor.React(action)
})
appClientset.AddReactor("patch", "applications", reaction)
h := NewHandler(
"argocd",
[]string{},
10,
appClientset,
&fakeAppsLister{clientset: appClientset},
&settings.ArgoCDSettings{},
&fakeSettingsSrc{},
repoCache,
serverCache,
mockDB,
int64(50)*1024*1024,
)
// Create payload with the changed file
payload := createTestPayload(ttc.changedFile)
req := httptest.NewRequest(http.MethodPost, "/api/webhook", http.NoBody)
req.Header.Set("X-GitHub-Event", "push")
req.Body = io.NopCloser(bytes.NewReader(payload))
w := httptest.NewRecorder()
h.Handler(w, req)
close(h.queue)
h.Wait()
assert.Equal(t, http.StatusOK, w.Code)
// Verify refresh behavior
assert.Equal(t, ttc.hasRefresh, patched, "patch status mismatch for test: %s", ttc.name)
if patched && patchData != nil {
verifyAnnotations(t, patchData, ttc.hasRefresh, ttc.hasHydrate)
}
// Verify cache update behavior
if ttc.updateCache {
var source *v1alpha1.ApplicationSource
if ttc.app.Spec.SourceHydrator != nil {
drySource := ttc.app.Spec.SourceHydrator.GetDrySource()
source = &drySource
} else if len(ttc.app.Spec.Sources) > 0 {
source = &ttc.app.Spec.Sources[0]
}
if source != nil {
// Verify cache was updated with afterSHA
clusterInfo := &mockClusterInfo{}
var afterManifests cache.CachedManifestResponse
err := repoCache.GetManifests(testAfterSHA, source, nil, clusterInfo, "", "", testAppLabelKey, ttc.app.Name, &afterManifests, nil, "")
require.NoError(t, err, "cache should be updated with afterSHA")
if err == nil {
assert.Equal(t, testAfterSHA, afterManifests.ManifestResponse.Revision, "cached revision should match afterSHA")
}
}
}
})
}
}
// createTestPayload creates a GitHub push event payload with the specified changed file
func createTestPayload(changedFile string) []byte {
payload := fmt.Sprintf(`{
"ref": "refs/heads/master",
"before": "%s",
"after": "%s",
"repository": {
"html_url": "https://github.com/jessesuen/test-repo",
"default_branch": "master"
},
"commits": [
{
"added": [],
"modified": ["%s"],
"removed": []
}
]
}`, testBeforeSHA, testAfterSHA, changedFile)
return []byte(payload)
}
func Test_affectedRevisionInfo_bitbucket_changed_files(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
@@ -922,10 +1257,9 @@ func Test_affectedRevisionInfo_bitbucket_changed_files(t *testing.T) {
"oldHash": "abcdef",
"newHash": "ghijkl",
})
if err != nil {
require.NoError(t, err)
}
_ = json.Unmarshal(doc.Bytes(), &pl)
require.NoError(t, err)
err = json.Unmarshal(doc.Bytes(), &pl)
require.NoError(t, err)
return pl
}
@@ -1238,3 +1572,72 @@ func getDiffstatResponderFn() func(req *http.Request) (*http.Response, error) {
return resp, nil
}
}
// mockClusterInfo implements cache.ClusterRuntimeInfo for testing
type mockClusterInfo struct{}
func (m *mockClusterInfo) GetApiVersions() []string { return []string{} } //nolint:revive // interface method name
func (m *mockClusterInfo) GetKubeVersion() string { return "1.28.0" }
// Common test constants
const (
testBeforeSHA = "d5c1ffa8e294bc18c639bfb4e0df499251034414"
testAfterSHA = "63738bb582c8b540af7bcfc18f87c575c3ed66e0"
testClusterURL = "https://kubernetes.default.svc"
testAppLabelKey = "mycompany.com/appname"
)
// verifyAnnotations is a helper that checks if the expected annotations are present in patch data
func verifyAnnotations(t *testing.T, patchData []byte, expectRefresh bool, expectHydrate bool) {
t.Helper()
if patchData == nil {
if expectRefresh {
t.Error("expected app to be patched but patchData is nil")
}
return
}
var patchMap map[string]any
err := json.Unmarshal(patchData, &patchMap)
require.NoError(t, err)
metadata, hasMetadata := patchMap["metadata"].(map[string]any)
require.True(t, hasMetadata, "patch should have metadata")
annotations, hasAnnotations := metadata["annotations"].(map[string]any)
require.True(t, hasAnnotations, "patch should have annotations")
// Check refresh annotation
refreshValue, hasRefresh := annotations["argocd.argoproj.io/refresh"]
if expectRefresh {
assert.True(t, hasRefresh, "should have refresh annotation")
assert.Equal(t, "normal", refreshValue, "refresh annotation should be 'normal'")
} else {
assert.False(t, hasRefresh, "should not have refresh annotation")
}
// Check hydrate annotation
hydrateValue, hasHydrate := annotations["argocd.argoproj.io/hydrate"]
if expectHydrate {
assert.True(t, hasHydrate, "should have hydrate annotation")
assert.Equal(t, "normal", hydrateValue, "hydrate annotation should be 'normal'")
} else {
assert.False(t, hasHydrate, "should not have hydrate annotation")
}
}
// setupTestCache is a helper that creates and populates a test cache
func setupTestCache(t *testing.T, repoCache *cache.Cache, appName string, source *v1alpha1.ApplicationSource, manifests []string) {
t.Helper()
clusterInfo := &mockClusterInfo{}
dummyManifests := &cache.CachedManifestResponse{
ManifestResponse: &apiclient.ManifestResponse{
Revision: testBeforeSHA,
Manifests: manifests,
Namespace: "",
Server: testClusterURL,
},
}
err := repoCache.SetManifests(testBeforeSHA, source, nil, clusterInfo, "", "", testAppLabelKey, appName, dummyManifests, nil, "")
require.NoError(t, err)
}