mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-03-10 10:28:47 +01:00
Compare commits
39 Commits
v3.1.7
...
release-3.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12c8d42f4f | ||
|
|
c19d63446d | ||
|
|
a46152e850 | ||
|
|
28b618775d | ||
|
|
1b7cfc0c06 | ||
|
|
0262c8ff97 | ||
|
|
cc053b2eeb | ||
|
|
f9adb4e7f4 | ||
|
|
6c3f3eab5d | ||
|
|
f22ccc2192 | ||
|
|
b424210b52 | ||
|
|
955ea1b1df | ||
|
|
836d47f311 | ||
|
|
db2004f2e2 | ||
|
|
b68c964321 | ||
|
|
f4f21b3642 | ||
|
|
38c15ada45 | ||
|
|
11e7758ca9 | ||
|
|
f2f4f4579b | ||
|
|
f9ada0403d | ||
|
|
b6660a2a7a | ||
|
|
787f3ec6a2 | ||
|
|
49ee0040c4 | ||
|
|
7eca62c2d6 | ||
|
|
8665140f96 | ||
|
|
a419e477e6 | ||
|
|
e53196f9fd | ||
|
|
16ba5f9c43 | ||
|
|
1904de5065 | ||
|
|
becb020064 | ||
|
|
c63c2d8909 | ||
|
|
e20828f869 | ||
|
|
761fc27068 | ||
|
|
1a023f1ca7 | ||
|
|
5c466a4e39 | ||
|
|
b2fa7dcde6 | ||
|
|
38808d03cd | ||
|
|
41eac62eac | ||
|
|
54bab39a80 |
18
.github/workflows/ci-build.yaml
vendored
18
.github/workflows/ci-build.yaml
vendored
@@ -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
|
||||
@@ -496,7 +496,7 @@ jobs:
|
||||
run: |
|
||||
docker pull ghcr.io/dexidp/dex:v2.43.0
|
||||
docker pull argoproj/argo-cd-ci-builder:v1.0.0
|
||||
docker pull redis:7.2.7-alpine
|
||||
docker pull redis:7.2.11-alpine
|
||||
- name: Create target directory for binaries in the build-process
|
||||
run: |
|
||||
mkdir -p dist
|
||||
|
||||
4
.github/workflows/image.yaml
vendored
4
.github/workflows/image.yaml
vendored
@@ -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:
|
||||
|
||||
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ const (
|
||||
var defaultPreservedAnnotations = []string{
|
||||
NotifiedAnnotationKey,
|
||||
argov1alpha1.AnnotationKeyRefresh,
|
||||
argov1alpha1.AnnotationKeyHydrate,
|
||||
}
|
||||
|
||||
// ApplicationSetReconciler reconciles a ApplicationSet object
|
||||
@@ -231,6 +232,16 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
|
||||
return ctrl.Result{}, fmt.Errorf("failed to perform progressive sync reconciliation for application set: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Progressive Sync is disabled, clear any existing applicationStatus to prevent stale data
|
||||
if len(applicationSetInfo.Status.ApplicationStatus) > 0 {
|
||||
logCtx.Infof("Progressive Sync disabled, removing %v AppStatus entries from ApplicationSet %v", len(applicationSetInfo.Status.ApplicationStatus), applicationSetInfo.Name)
|
||||
|
||||
err := r.setAppSetApplicationStatus(ctx, logCtx, &applicationSetInfo, []argov1alpha1.ApplicationSetApplicationStatus{})
|
||||
if err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("failed to clear AppSet application statuses when Progressive Sync is disabled for %v: %w", applicationSetInfo.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var validApps []argov1alpha1.Application
|
||||
@@ -565,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)
|
||||
}
|
||||
|
||||
@@ -589,6 +589,72 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Ensure that hydrate annotation is preserved from an existing app",
|
||||
appSet: v1alpha1.ApplicationSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "name",
|
||||
Namespace: "namespace",
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
Template: v1alpha1.ApplicationSetTemplate{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingApps: []v1alpha1.Application{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: application.ApplicationKind,
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app1",
|
||||
Namespace: "namespace",
|
||||
ResourceVersion: "2",
|
||||
Annotations: map[string]string{
|
||||
"annot-key": "annot-value",
|
||||
v1alpha1.AnnotationKeyHydrate: string(v1alpha1.RefreshTypeNormal),
|
||||
},
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
desiredApps: []v1alpha1.Application{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app1",
|
||||
Namespace: "namespace",
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []v1alpha1.Application{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: application.ApplicationKind,
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app1",
|
||||
Namespace: "namespace",
|
||||
ResourceVersion: "3",
|
||||
Annotations: map[string]string{
|
||||
v1alpha1.AnnotationKeyHydrate: string(v1alpha1.RefreshTypeNormal),
|
||||
},
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Ensure that configured preserved annotations are preserved from an existing app",
|
||||
appSet: v1alpha1.ApplicationSet{
|
||||
@@ -7319,3 +7385,82 @@ func TestSyncApplication(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileProgressiveSyncDisabled(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
err := v1alpha1.AddToScheme(scheme)
|
||||
require.NoError(t, err)
|
||||
|
||||
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
|
||||
|
||||
for _, cc := range []struct {
|
||||
name string
|
||||
appSet v1alpha1.ApplicationSet
|
||||
enableProgressiveSyncs bool
|
||||
expectedAppStatuses []v1alpha1.ApplicationSetApplicationStatus
|
||||
}{
|
||||
{
|
||||
name: "clears applicationStatus when Progressive Sync is disabled",
|
||||
appSet: v1alpha1.ApplicationSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-appset",
|
||||
Namespace: "argocd",
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
Generators: []v1alpha1.ApplicationSetGenerator{},
|
||||
Template: v1alpha1.ApplicationSetTemplate{},
|
||||
},
|
||||
Status: v1alpha1.ApplicationSetStatus{
|
||||
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
|
||||
{
|
||||
Application: "test-appset-guestbook",
|
||||
Message: "Application resource became Healthy, updating status from Progressing to Healthy.",
|
||||
Status: "Healthy",
|
||||
Step: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
enableProgressiveSyncs: false,
|
||||
expectedAppStatuses: nil,
|
||||
},
|
||||
} {
|
||||
t.Run(cc.name, func(t *testing.T) {
|
||||
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&cc.appSet).WithStatusSubresource(&cc.appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
|
||||
metrics := appsetmetrics.NewFakeAppsetMetrics()
|
||||
|
||||
argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
|
||||
|
||||
r := ApplicationSetReconciler{
|
||||
Client: client,
|
||||
Scheme: scheme,
|
||||
Renderer: &utils.Render{},
|
||||
Recorder: record.NewFakeRecorder(1),
|
||||
Generators: map[string]generators.Generator{},
|
||||
ArgoDB: argodb,
|
||||
KubeClientset: kubeclientset,
|
||||
Metrics: metrics,
|
||||
EnableProgressiveSyncs: cc.enableProgressiveSyncs,
|
||||
}
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: cc.appSet.Namespace,
|
||||
Name: cc.appSet.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Run reconciliation
|
||||
_, err = r.Reconcile(t.Context(), req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Fetch the updated ApplicationSet
|
||||
var updatedAppSet v1alpha1.ApplicationSet
|
||||
err = r.Get(t.Context(), req.NamespacedName, &updatedAppSet)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the applicationStatus field
|
||||
assert.Equal(t, cc.expectedAppStatuses, updatedAppSet.Status.ApplicationStatus, "applicationStatus should match expected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -26,10 +26,14 @@ import (
|
||||
"github.com/go-playground/webhooks/v6/github"
|
||||
"github.com/go-playground/webhooks/v6/gitlab"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/util/guard"
|
||||
)
|
||||
|
||||
const payloadQueueSize = 50000
|
||||
|
||||
const panicMsgAppSet = "panic while processing applicationset-controller webhook event"
|
||||
|
||||
type WebhookHandler struct {
|
||||
sync.WaitGroup // for testing
|
||||
github *github.Webhook
|
||||
@@ -102,6 +106,7 @@ func NewWebhookHandler(webhookParallelism int, argocdSettingsMgr *argosettings.S
|
||||
}
|
||||
|
||||
func (h *WebhookHandler) startWorkerPool(webhookParallelism int) {
|
||||
compLog := log.WithField("component", "applicationset-webhook")
|
||||
for i := 0; i < webhookParallelism; i++ {
|
||||
h.Add(1)
|
||||
go func() {
|
||||
@@ -111,7 +116,7 @@ func (h *WebhookHandler) startWorkerPool(webhookParallelism int) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h.HandleEvent(payload)
|
||||
guard.RecoverAndLog(func() { h.HandleEvent(payload) }, compLog, panicMsgAppSet)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -35,14 +35,26 @@ metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
trigger.sync-operation-change: |
|
||||
- when: app.status.operationState.phase in ['Succeeded']
|
||||
- when: app.status?.operationState.phase in ['Succeeded']
|
||||
send: [github-commit-status]
|
||||
- when: app.status.operationState.phase in ['Running']
|
||||
- when: app.status?.operationState.phase in ['Running']
|
||||
send: [github-commit-status]
|
||||
- when: app.status.operationState.phase in ['Error', 'Failed']
|
||||
- when: app.status?.operationState.phase in ['Error', 'Failed']
|
||||
send: [app-sync-failed, github-commit-status]
|
||||
```
|
||||
|
||||
|
||||
## Accessing Optional Manifest Sections and Fields
|
||||
|
||||
Note that in the trigger example above, the `?.` (optional chaining) operator is used to access the Application's
|
||||
`status.operationState` section. This section is optional; it is not present when an operation has been initiated but has not yet
|
||||
started by the Application Controller.
|
||||
|
||||
If the `?.` operator were not used, `status.operationState` would resolve to `nil` and the evaluation of the
|
||||
`app.status.operationState.phase` expression would fail. The `app.status?.operationState.phase` expression is equivalent to
|
||||
`app.status.operationState != nil ? app.status.operationState.phase : nil`.
|
||||
|
||||
|
||||
## Avoid Sending Same Notification Too Often
|
||||
|
||||
In some cases, the trigger condition might be "flapping". The example below illustrates the problem.
|
||||
@@ -60,14 +72,14 @@ data:
|
||||
# Optional 'oncePer' property ensure that notification is sent only once per specified field value
|
||||
# E.g. following is triggered once per sync revision
|
||||
trigger.on-deployed: |
|
||||
when: app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy'
|
||||
when: app.status?.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy'
|
||||
oncePer: app.status.sync.revision
|
||||
send: [app-sync-succeeded]
|
||||
```
|
||||
|
||||
**Mono Repo Usage**
|
||||
|
||||
When one repo is used to sync multiple applications, the `oncePer: app.status.sync.revision` field will trigger a notification for each commit. For mono repos, the better approach will be using `oncePer: app.status.operationState.syncResult.revision` statement. This way a notification will be sent only for a particular Application's revision.
|
||||
When one repo is used to sync multiple applications, the `oncePer: app.status.sync.revision` field will trigger a notification for each commit. For mono repos, the better approach will be using `oncePer: app.status?.operationState.syncResult.revision` statement. This way a notification will be sent only for a particular Application's revision.
|
||||
|
||||
### oncePer
|
||||
|
||||
@@ -122,7 +134,7 @@ Triggers have access to the set of built-in functions.
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
when: time.Now().Sub(time.Parse(app.status.operationState.startedAt)).Minutes() >= 5
|
||||
when: time.Now().Sub(time.Parse(app.status?.operationState.startedAt)).Minutes() >= 5
|
||||
```
|
||||
|
||||
{!docs/operator-manual/notifications/functions.md!}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -288,6 +288,9 @@ resources.
|
||||
delete it. To avoid this edge case, it is recommended to perform a sync operation on your Applications, even if
|
||||
they are not out of sync, so that orphan resource detection will work as expected on the next sync.
|
||||
|
||||
After upgrading to version 3.0, the Argo CD tracking annotation will only appear on an Application’s resources when
|
||||
either a new Git commit is made or the Application is explicitly synced.
|
||||
|
||||
##### Users who rely on label-based for resources that are not managed by Argo CD
|
||||
Some users rely on label-based tracking to track resources that are not managed by Argo CD. They may set annotations
|
||||
to have Argo CD ignore the resource as extraneous or to disable pruning. If you are using label-based tracking to track
|
||||
@@ -497,4 +500,4 @@ More details for ignored resource updates in the [Diffing customization](../../u
|
||||
|
||||
Due to security reasons ([GHSA-786q-9hcg-v9ff](https://github.com/argoproj/argo-cd/security/advisories/GHSA-786q-9hcg-v9ff)),
|
||||
the project API response was sanitized to remove sensitive information. This includes
|
||||
credentials of project-scoped repositories and clusters.
|
||||
credentials of project-scoped repositories and clusters.
|
||||
|
||||
19
go.mod
19
go.mod
@@ -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
|
||||
@@ -92,11 +92,11 @@ require (
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0
|
||||
go.opentelemetry.io/otel/sdk v1.36.0
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.15.0
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/term v0.38.0
|
||||
golang.org/x/time v0.12.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237
|
||||
google.golang.org/grpc v1.73.0
|
||||
@@ -266,10 +266,11 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
|
||||
gomodules.xyz/envconfig v1.3.1-0.20190308184047-426f31af0d45 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
gomodules.xyz/notify v0.1.1 // indirect
|
||||
|
||||
40
go.sum
40
go.sum
@@ -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=
|
||||
@@ -977,8 +977,8 @@ golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1026,8 +1026,8 @@ golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1095,8 +1095,8 @@ golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1126,8 +1126,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1210,8 +1210,8 @@ golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
@@ -1239,8 +1239,8 @@ golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1263,8 +1263,8 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1335,8 +1335,12 @@ golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY=
|
||||
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
@@ -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)
|
||||
|
||||
@@ -12,4 +12,4 @@ resources:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v3.1.7
|
||||
newTag: v3.1.12
|
||||
|
||||
@@ -5,7 +5,7 @@ kind: Kustomization
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v3.1.7
|
||||
newTag: v3.1.12
|
||||
resources:
|
||||
- ./application-controller
|
||||
- ./dex
|
||||
|
||||
@@ -40,7 +40,7 @@ spec:
|
||||
serviceAccountName: argocd-redis
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:7.2.7-alpine
|
||||
image: redis:7.2.11-alpine
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- "--save"
|
||||
|
||||
14
manifests/core-install-with-hydrator.yaml
generated
14
manifests/core-install-with-hydrator.yaml
generated
@@ -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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -24943,7 +24943,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: Always
|
||||
name: redis
|
||||
ports:
|
||||
@@ -24959,7 +24959,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.1.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
12
manifests/core-install.yaml
generated
12
manifests/core-install.yaml
generated
@@ -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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -24777,7 +24777,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: Always
|
||||
name: redis
|
||||
ports:
|
||||
@@ -24793,7 +24793,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.1.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -12,4 +12,4 @@ resources:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v3.1.7
|
||||
newTag: v3.1.12
|
||||
|
||||
@@ -12,7 +12,7 @@ patches:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v3.1.7
|
||||
newTag: v3.1.12
|
||||
resources:
|
||||
- ../../base/application-controller
|
||||
- ../../base/applicationset-controller
|
||||
|
||||
@@ -1250,7 +1250,7 @@ spec:
|
||||
automountServiceAccountToken: false
|
||||
initContainers:
|
||||
- name: config-init
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
resources:
|
||||
{}
|
||||
@@ -1290,7 +1290,7 @@ spec:
|
||||
|
||||
containers:
|
||||
- name: redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- redis-server
|
||||
@@ -1364,7 +1364,7 @@ spec:
|
||||
- /bin/sh
|
||||
- /readonly-config/trigger-failover-if-master.sh
|
||||
- name: sentinel
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- redis-sentinel
|
||||
@@ -1437,7 +1437,7 @@ spec:
|
||||
- sleep 30; redis-cli -p 26379 sentinel reset argocd
|
||||
|
||||
- name: split-brain-fix
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- sh
|
||||
|
||||
@@ -27,7 +27,7 @@ redis-ha:
|
||||
serviceAccount:
|
||||
automountToken: true
|
||||
image:
|
||||
tag: 7.2.7-alpine
|
||||
tag: 7.2.11-alpine
|
||||
sentinel:
|
||||
bind: '0.0.0.0'
|
||||
lifecycle:
|
||||
|
||||
26
manifests/ha/install-with-hydrator.yaml
generated
26
manifests/ha/install-with-hydrator.yaml
generated
@@ -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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
@@ -27787,7 +27787,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
lifecycle:
|
||||
preStop:
|
||||
@@ -27858,7 +27858,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
lifecycle:
|
||||
postStart:
|
||||
@@ -27933,7 +27933,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: split-brain-fix
|
||||
resources: {}
|
||||
@@ -27968,7 +27968,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: config-init
|
||||
securityContext:
|
||||
|
||||
24
manifests/ha/install.yaml
generated
24
manifests/ha/install.yaml
generated
@@ -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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
@@ -27623,7 +27623,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
lifecycle:
|
||||
preStop:
|
||||
@@ -27694,7 +27694,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
lifecycle:
|
||||
postStart:
|
||||
@@ -27769,7 +27769,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: split-brain-fix
|
||||
resources: {}
|
||||
@@ -27804,7 +27804,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: config-init
|
||||
securityContext:
|
||||
|
||||
26
manifests/ha/namespace-install-with-hydrator.yaml
generated
26
manifests/ha/namespace-install-with-hydrator.yaml
generated
@@ -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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
@@ -3590,7 +3590,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
lifecycle:
|
||||
preStop:
|
||||
@@ -3661,7 +3661,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
lifecycle:
|
||||
postStart:
|
||||
@@ -3736,7 +3736,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: split-brain-fix
|
||||
resources: {}
|
||||
@@ -3771,7 +3771,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: config-init
|
||||
securityContext:
|
||||
|
||||
24
manifests/ha/namespace-install.yaml
generated
24
manifests/ha/namespace-install.yaml
generated
@@ -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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
@@ -3426,7 +3426,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
lifecycle:
|
||||
preStop:
|
||||
@@ -3497,7 +3497,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
lifecycle:
|
||||
postStart:
|
||||
@@ -3572,7 +3572,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: split-brain-fix
|
||||
resources: {}
|
||||
@@ -3607,7 +3607,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: config-init
|
||||
securityContext:
|
||||
|
||||
20
manifests/install-with-hydrator.yaml
generated
20
manifests/install-with-hydrator.yaml
generated
@@ -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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -25624,7 +25624,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: Always
|
||||
name: redis
|
||||
ports:
|
||||
@@ -25640,7 +25640,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.1.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
18
manifests/install.yaml
generated
18
manifests/install.yaml
generated
@@ -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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -25458,7 +25458,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: Always
|
||||
name: redis
|
||||
ports:
|
||||
@@ -25474,7 +25474,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.1.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
20
manifests/namespace-install-with-hydrator.yaml
generated
20
manifests/namespace-install-with-hydrator.yaml
generated
@@ -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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -1427,7 +1427,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: Always
|
||||
name: redis
|
||||
ports:
|
||||
@@ -1443,7 +1443,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.1.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
18
manifests/namespace-install.yaml
generated
18
manifests/namespace-install.yaml
generated
@@ -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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -1261,7 +1261,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: auth
|
||||
name: argocd-redis
|
||||
image: public.ecr.aws/docker/library/redis:7.2.7-alpine
|
||||
image: public.ecr.aws/docker/library/redis:7.2.11-alpine
|
||||
imagePullPolicy: Always
|
||||
name: redis
|
||||
ports:
|
||||
@@ -1277,7 +1277,7 @@ spec:
|
||||
- argocd
|
||||
- admin
|
||||
- redis-initial-password
|
||||
image: quay.io/argoproj/argocd:v3.1.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
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.7
|
||||
image: quay.io/argoproj/argocd:v3.1.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -131,6 +131,7 @@ nav:
|
||||
- operator-manual/server-commands/additional-configuration-method.md
|
||||
- Upgrading:
|
||||
- operator-manual/upgrading/overview.md
|
||||
- operator-manual/upgrading/3.0-3.1.md
|
||||
- operator-manual/upgrading/2.14-3.0.md
|
||||
- operator-manual/upgrading/2.13-2.14.md
|
||||
- operator-manual/upgrading/2.12-2.13.md
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- Health check copied from here: https://github.com/crossplane/docs/blob/bd701357e9d5eecf529a0b42f23a78850a6d1d87/content/master/guides/crossplane-with-argo-cd.md
|
||||
-- Health check copied from here: https://github.com/crossplane/docs/blob/709889c5dbe6e5a2ea3dffd66fe276cf465b47b5/content/master/guides/crossplane-with-argo-cd.md
|
||||
|
||||
health_status = {
|
||||
status = "Progressing",
|
||||
@@ -18,9 +18,10 @@ local has_no_status = {
|
||||
"Composition",
|
||||
"CompositionRevision",
|
||||
"DeploymentRuntimeConfig",
|
||||
"ControllerConfig",
|
||||
"ClusterProviderConfig",
|
||||
"ProviderConfig",
|
||||
"ProviderConfigUsage"
|
||||
"ProviderConfigUsage",
|
||||
"ControllerConfig" -- Added to ensure that healthcheck is backwards-compatible with Crossplane v1
|
||||
}
|
||||
if obj.status == nil or next(obj.status) == nil and contains(has_no_status, obj.kind) then
|
||||
health_status.status = "Healthy"
|
||||
@@ -29,7 +30,7 @@ if obj.status == nil or next(obj.status) == nil and contains(has_no_status, obj.
|
||||
end
|
||||
|
||||
if obj.status == nil or next(obj.status) == nil or obj.status.conditions == nil then
|
||||
if obj.kind == "ProviderConfig" and obj.status.users ~= nil then
|
||||
if (obj.kind == "ProviderConfig" or obj.kind == "ClusterProviderConfig") and obj.status.users ~= nil then
|
||||
health_status.status = "Healthy"
|
||||
health_status.message = "Resource is in use."
|
||||
return health_status
|
||||
@@ -54,7 +55,7 @@ for i, condition in ipairs(obj.status.conditions) do
|
||||
end
|
||||
end
|
||||
|
||||
if contains({"Ready", "Healthy", "Offered", "Established"}, condition.type) then
|
||||
if contains({"Ready", "Healthy", "Offered", "Established", "ValidPipeline", "RevisionHealthy"}, condition.type) then
|
||||
if condition.status == "True" then
|
||||
health_status.status = "Healthy"
|
||||
health_status.message = "Resource is up-to-date."
|
||||
|
||||
@@ -3,3 +3,7 @@ tests:
|
||||
status: Healthy
|
||||
message: "Resource is up-to-date."
|
||||
inputPath: testdata/composition_healthy.yaml
|
||||
- healthStatus:
|
||||
status: Healthy
|
||||
message: "Resource is up-to-date."
|
||||
inputPath: testdata/configurationrevision_healthy.yaml
|
||||
22
resource_customizations/_.crossplane.io/_/testdata/configurationrevision_healthy.yaml
vendored
Normal file
22
resource_customizations/_.crossplane.io/_/testdata/configurationrevision_healthy.yaml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: pkg.crossplane.io/v1
|
||||
kind: ConfigurationRevision
|
||||
metadata:
|
||||
annotations:
|
||||
meta.crossplane.io/license: Apache-2.0
|
||||
meta.crossplane.io/maintainer: Upbound <support@upbound.io>
|
||||
meta.crossplane.io/source: github.com/upbound/configuration-getting-started
|
||||
name: upbound-configuration-getting-started-869bca254eb1
|
||||
spec:
|
||||
desiredState: Active
|
||||
ignoreCrossplaneConstraints: false
|
||||
image: xpkg.upbound.io/upbound/configuration-getting-started:v0.3.0
|
||||
packagePullPolicy: IfNotPresent
|
||||
revision: 1
|
||||
skipDependencyResolution: false
|
||||
status:
|
||||
conditions:
|
||||
- lastTransitionTime: "2025-09-29T18:06:40Z"
|
||||
observedGeneration: 1
|
||||
reason: HealthyPackageRevision
|
||||
status: "True"
|
||||
type: RevisionHealthy
|
||||
@@ -1,4 +1,4 @@
|
||||
-- Health check copied from here: https://github.com/crossplane/docs/blob/bd701357e9d5eecf529a0b42f23a78850a6d1d87/content/master/guides/crossplane-with-argo-cd.md
|
||||
-- Health check copied from here: https://github.com/crossplane/docs/blob/709889c5dbe6e5a2ea3dffd66fe276cf465b47b5/content/master/guides/crossplane-with-argo-cd.md
|
||||
|
||||
health_status = {
|
||||
status = "Progressing",
|
||||
@@ -15,6 +15,7 @@ local function contains (table, val)
|
||||
end
|
||||
|
||||
local has_no_status = {
|
||||
"ClusterProviderConfig",
|
||||
"ProviderConfig",
|
||||
"ProviderConfigUsage"
|
||||
}
|
||||
@@ -26,7 +27,7 @@ if obj.status == nil or next(obj.status) == nil and contains(has_no_status, obj.
|
||||
end
|
||||
|
||||
if obj.status == nil or next(obj.status) == nil or obj.status.conditions == nil then
|
||||
if obj.kind == "ProviderConfig" and obj.status.users ~= nil then
|
||||
if (obj.kind == "ProviderConfig" or obj.kind == "ClusterProviderConfig") and obj.status.users ~= nil then
|
||||
health_status.status = "Healthy"
|
||||
health_status.message = "Resource is in use."
|
||||
return health_status
|
||||
|
||||
@@ -7,3 +7,6 @@ discoveryTests:
|
||||
- inputPath: testdata/external-secret.yaml
|
||||
result:
|
||||
- name: "refresh"
|
||||
- inputPath: testdata/external-secret-refresh-policy.yaml
|
||||
result:
|
||||
- name: "refresh"
|
||||
|
||||
@@ -3,10 +3,11 @@ local actions = {}
|
||||
local disable_refresh = false
|
||||
local time_units = {"ns", "us", "µs", "ms", "s", "m", "h"}
|
||||
local digits = obj.spec.refreshInterval
|
||||
local policy = obj.spec.refreshPolicy
|
||||
if digits ~= nil then
|
||||
digits = tostring(digits)
|
||||
for _, time_unit in ipairs(time_units) do
|
||||
if digits == "0" or digits == "0" .. time_unit then
|
||||
if (digits == "0" or digits == "0" .. time_unit) and policy ~= "OnChange" then
|
||||
disable_refresh = true
|
||||
break
|
||||
end
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
apiVersion: external-secrets.io/v1alpha1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
creationTimestamp: '2021-11-16T21:59:33Z'
|
||||
generation: 1
|
||||
name: test-healthy
|
||||
namespace: argocd
|
||||
resourceVersion: '136487331'
|
||||
selfLink: /apis/external-secrets.io/v1alpha1/namespaces/argocd/externalsecrets/test-healthy
|
||||
uid: 1e754a7e-0781-4d57-932d-4651d5b19586
|
||||
spec:
|
||||
data:
|
||||
- remoteRef:
|
||||
key: secret/sa/example
|
||||
property: api.address
|
||||
secretKey: url
|
||||
- remoteRef:
|
||||
key: secret/sa/example
|
||||
property: ca.crt
|
||||
secretKey: ca
|
||||
- remoteRef:
|
||||
key: secret/sa/example
|
||||
property: token
|
||||
secretKey: token
|
||||
refreshInterval: 0
|
||||
refreshPolicy: OnChange
|
||||
secretStoreRef:
|
||||
kind: SecretStore
|
||||
name: example
|
||||
target:
|
||||
creationPolicy: Owner
|
||||
template:
|
||||
data:
|
||||
config: |
|
||||
{
|
||||
"bearerToken": "{{ .token | base64decode | toString }}",
|
||||
"tlsClientConfig": {
|
||||
"insecure": false,
|
||||
"caData": "{{ .ca | toString }}"
|
||||
}
|
||||
}
|
||||
name: cluster-test
|
||||
server: '{{ .url | toString }}'
|
||||
metadata:
|
||||
labels:
|
||||
argocd.argoproj.io/secret-type: cluster
|
||||
status:
|
||||
conditions:
|
||||
- lastTransitionTime: '2021-11-16T21:59:34Z'
|
||||
message: Secret was synced
|
||||
reason: SecretSynced
|
||||
status: 'True'
|
||||
type: Ready
|
||||
refreshTime: '2021-11-29T18:32:24Z'
|
||||
syncedResourceVersion: 1-519a61da0dc68b2575b4f8efada70e42
|
||||
@@ -9,10 +9,39 @@ function checkConditions(conditions, conditionType)
|
||||
return true
|
||||
end
|
||||
|
||||
-- isParentGenerationObserved checks if a parent's conditions match the current resource generation
|
||||
-- For HTTPRoute, observedGeneration is stored in each condition within a parent
|
||||
function isParentGenerationObserved(obj, parent)
|
||||
if obj.metadata.generation == nil then
|
||||
-- If no generation is set, accept all conditions
|
||||
return true
|
||||
end
|
||||
|
||||
if parent.conditions == nil or #parent.conditions == 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Check if all conditions have observedGeneration matching current generation
|
||||
for _, condition in ipairs(parent.conditions) do
|
||||
if condition.observedGeneration ~= nil then
|
||||
if condition.observedGeneration ~= obj.metadata.generation then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if obj.status ~= nil then
|
||||
if obj.status.parents ~= nil then
|
||||
for _, parent in ipairs(obj.status.parents) do
|
||||
if parent.conditions ~= nil then
|
||||
-- Skip this parent if it's not from the current generation
|
||||
if not isParentGenerationObserved(obj, parent) then
|
||||
goto continue
|
||||
end
|
||||
|
||||
local resolvedRefsFalse, resolvedRefsMsg = checkConditions(parent.conditions, "ResolvedRefs")
|
||||
local acceptedFalse, acceptedMsg = checkConditions(parent.conditions, "Accepted")
|
||||
|
||||
@@ -44,15 +73,20 @@ if obj.status ~= nil then
|
||||
hs.message = "Parent " .. (parent.parentRef.name or "") .. ": " .. progressingMsg
|
||||
return hs
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
end
|
||||
|
||||
if #obj.status.parents > 0 then
|
||||
for _, parent in ipairs(obj.status.parents) do
|
||||
if parent.conditions ~= nil and #parent.conditions > 0 then
|
||||
hs.status = "Healthy"
|
||||
hs.message = "HTTPRoute is healthy"
|
||||
return hs
|
||||
-- Only mark as healthy if we found a parent from the current generation
|
||||
if isParentGenerationObserved(obj, parent) then
|
||||
hs.status = "Healthy"
|
||||
hs.message = "HTTPRoute is healthy"
|
||||
return hs
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,4 +14,8 @@ tests:
|
||||
- healthStatus:
|
||||
status: Progressing
|
||||
message: "Parent example-gateway: Route is still being programmed"
|
||||
inputPath: testdata/progressing.yaml
|
||||
inputPath: testdata/progressing.yaml
|
||||
- healthStatus:
|
||||
status: Healthy
|
||||
message: HTTPRoute is healthy
|
||||
inputPath: testdata/healthy_multiple_generations.yaml
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: example-httproute
|
||||
generation: 2
|
||||
spec:
|
||||
parentRefs:
|
||||
- kind: Gateway
|
||||
name: eg
|
||||
namespace: envoy-gateway-system
|
||||
sectionName: foo-nonexistent
|
||||
hostnames:
|
||||
- "example-httproute.example.com"
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: example-service
|
||||
port: 8080
|
||||
status:
|
||||
parents:
|
||||
- conditions:
|
||||
- lastTransitionTime: "2025-10-14T11:19:41Z"
|
||||
message: No listeners match this parent ref
|
||||
observedGeneration: 1
|
||||
reason: NoMatchingParent
|
||||
status: "False"
|
||||
type: Accepted
|
||||
- lastTransitionTime: "2025-10-14T11:19:41Z"
|
||||
message: Resolved all the Object references for the Route
|
||||
observedGeneration: 1
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: gateway.envoyproxy.io/gatewayclass-controller
|
||||
parentRef:
|
||||
group: gateway.networking.k8s.io
|
||||
kind: Gateway
|
||||
name: eg
|
||||
namespace: envoy-gateway-system
|
||||
sectionName: foo-nonexistent
|
||||
- conditions:
|
||||
- lastTransitionTime: "2025-10-14T11:25:18Z"
|
||||
message: Route is accepted
|
||||
observedGeneration: 2
|
||||
reason: Accepted
|
||||
status: "True"
|
||||
type: Accepted
|
||||
- lastTransitionTime: "2025-10-14T11:25:18Z"
|
||||
message: Resolved all the Object references for the Route
|
||||
observedGeneration: 2
|
||||
reason: ResolvedRefs
|
||||
status: "True"
|
||||
type: ResolvedRefs
|
||||
controllerName: gateway.envoyproxy.io/gatewayclass-controller
|
||||
parentRef:
|
||||
group: gateway.networking.k8s.io
|
||||
kind: Gateway
|
||||
name: eg
|
||||
namespace: envoy-gateway-system
|
||||
sectionName: https-net
|
||||
@@ -24,14 +24,23 @@ if obj.status ~= nil then
|
||||
|
||||
if obj.status.conditions ~= nil then
|
||||
for i, condition in pairs(obj.status.conditions) do
|
||||
-- Check if the InferenceService is Stopped
|
||||
if condition.type == "Stopped" and condition.status == "True" then
|
||||
health_status.status = "Suspended"
|
||||
health_status.message = "InferenceService is Stopped"
|
||||
return health_status
|
||||
end
|
||||
|
||||
-- Check for unhealthy statuses
|
||||
-- Note: The Stopped condition's healthy status is False
|
||||
if condition.status == "Unknown" then
|
||||
status_unknown = status_unknown + 1
|
||||
elseif condition.status == "False" then
|
||||
elseif condition.status == "False" and condition.type ~= "Stopped" then
|
||||
status_false = status_false + 1
|
||||
end
|
||||
|
||||
if condition.status ~= "True" then
|
||||
-- Add the error messages if the status is unhealthy
|
||||
if condition.status ~= "True" and condition.type ~= "Stopped" then
|
||||
msg = msg .. " | " .. i .. ": " .. condition.type .. " | " .. condition.status
|
||||
if condition.reason ~= nil and condition.reason ~= "" then
|
||||
msg = msg .. " | " .. condition.reason
|
||||
|
||||
@@ -23,6 +23,10 @@ tests:
|
||||
status: Degraded
|
||||
message: "0: transitionStatus | BlockedByFailedLoad"
|
||||
inputPath: testdata/degraded_modelmesh.yaml
|
||||
- healthStatus:
|
||||
status: Suspended
|
||||
message: InferenceService is Stopped
|
||||
inputPath: testdata/stopped.yaml
|
||||
- healthStatus:
|
||||
status: Healthy
|
||||
message: InferenceService is healthy.
|
||||
|
||||
@@ -23,3 +23,7 @@ status:
|
||||
- lastTransitionTime: "2023-06-20T22:44:51Z"
|
||||
status: "True"
|
||||
type: Ready
|
||||
- lastTransitionTime: "2023-06-20T22:44:51Z"
|
||||
severity: Info
|
||||
status: 'False'
|
||||
type: Stopped
|
||||
|
||||
@@ -31,5 +31,9 @@ status:
|
||||
severity: Info
|
||||
status: 'True'
|
||||
type: RoutesReady
|
||||
- lastTransitionTime: '2024-05-30T22:14:31Z'
|
||||
severity: Info
|
||||
status: 'False'
|
||||
type: Stopped
|
||||
modelStatus:
|
||||
transitionStatus: UpToDate
|
||||
|
||||
@@ -17,3 +17,7 @@ status:
|
||||
- lastTransitionTime: '2024-05-16T18:48:56Z'
|
||||
status: 'True'
|
||||
type: Ready
|
||||
- lastTransitionTime: '2024-05-16T18:48:56Z'
|
||||
severity: Info
|
||||
status: 'False'
|
||||
type: Stopped
|
||||
|
||||
23
resource_customizations/serving.kserve.io/InferenceService/testdata/stopped.yaml
vendored
Normal file
23
resource_customizations/serving.kserve.io/InferenceService/testdata/stopped.yaml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
apiVersion: serving.kserve.io/v1beta1
|
||||
kind: InferenceService
|
||||
metadata:
|
||||
name: helloworld
|
||||
namespace: default
|
||||
annotations:
|
||||
serving.kserve.io/deploymentMode: RawDeployment
|
||||
serving.kserve.io/stop: 'true'
|
||||
spec: {}
|
||||
status:
|
||||
conditions:
|
||||
- lastTransitionTime: '2024-05-16T18:48:56Z'
|
||||
reason: Stopped
|
||||
status: 'False'
|
||||
type: PredictorReady
|
||||
- lastTransitionTime: '2024-05-16T18:48:56Z'
|
||||
reason: Stopped
|
||||
status: 'False'
|
||||
type: Ready
|
||||
- lastTransitionTime: '2024-05-16T18:48:56Z'
|
||||
severity: Info
|
||||
status: 'True'
|
||||
type: Stopped
|
||||
@@ -777,9 +777,8 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*v1a
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.inferResourcesStatusHealth(a)
|
||||
|
||||
if q.Refresh == nil {
|
||||
s.inferResourcesStatusHealth(a)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
@@ -862,7 +861,9 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*v1a
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
if _, ok := annotations[v1alpha1.AnnotationKeyRefresh]; !ok {
|
||||
return event.Application.DeepCopy(), nil
|
||||
refreshedApp := event.Application.DeepCopy()
|
||||
s.inferResourcesStatusHealth(refreshedApp)
|
||||
return refreshedApp, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2481,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) {
|
||||
|
||||
@@ -2539,6 +2539,99 @@ func TestGetAppRefresh_HardRefresh(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetApp_HealthStatusPropagation(t *testing.T) {
|
||||
newServerWithTree := func(t *testing.T) (*Server, *v1alpha1.Application) {
|
||||
t.Helper()
|
||||
cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour))
|
||||
|
||||
testApp := newTestApp()
|
||||
testApp.Status.ResourceHealthSource = v1alpha1.ResourceHealthLocationAppTree
|
||||
testApp.Status.Resources = []v1alpha1.ResourceStatus{
|
||||
{
|
||||
Group: "apps",
|
||||
Kind: "Deployment",
|
||||
Name: "guestbook",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
|
||||
appServer := newTestAppServer(t, testApp)
|
||||
|
||||
appStateCache := appstate.NewCache(cacheClient, time.Minute)
|
||||
appInstanceName := testApp.InstanceName(appServer.appNamespaceOrDefault(testApp.Namespace))
|
||||
err := appStateCache.SetAppResourcesTree(appInstanceName, &v1alpha1.ApplicationTree{
|
||||
Nodes: []v1alpha1.ResourceNode{{
|
||||
ResourceRef: v1alpha1.ResourceRef{
|
||||
Group: "apps",
|
||||
Kind: "Deployment",
|
||||
Name: "guestbook",
|
||||
Namespace: "default",
|
||||
},
|
||||
Health: &v1alpha1.HealthStatus{Status: health.HealthStatusDegraded},
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute)
|
||||
|
||||
return appServer, testApp
|
||||
}
|
||||
|
||||
t.Run("propagated health status on get with no refresh", func(t *testing.T) {
|
||||
appServer, testApp := newServerWithTree(t)
|
||||
fetchedApp, err := appServer.Get(t.Context(), &application.ApplicationQuery{
|
||||
Name: &testApp.Name,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, health.HealthStatusDegraded, fetchedApp.Status.Resources[0].Health.Status)
|
||||
})
|
||||
|
||||
t.Run("propagated health status on normal refresh", func(t *testing.T) {
|
||||
appServer, testApp := newServerWithTree(t)
|
||||
var patched int32
|
||||
ch := make(chan string, 1)
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch)
|
||||
|
||||
fetchedApp, err := appServer.Get(t.Context(), &application.ApplicationQuery{
|
||||
Name: &testApp.Name,
|
||||
Refresh: ptr.To(string(v1alpha1.RefreshTypeNormal)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
assert.Equal(t, int32(1), atomic.LoadInt32(&patched))
|
||||
case <-time.After(10 * time.Second):
|
||||
assert.Fail(t, "Out of time ( 10 seconds )")
|
||||
}
|
||||
assert.Equal(t, health.HealthStatusDegraded, fetchedApp.Status.Resources[0].Health.Status)
|
||||
})
|
||||
|
||||
t.Run("propagated health status on hard refresh", func(t *testing.T) {
|
||||
appServer, testApp := newServerWithTree(t)
|
||||
var patched int32
|
||||
ch := make(chan string, 1)
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
go refreshAnnotationRemover(t, ctx, &patched, appServer, testApp.Name, ch)
|
||||
|
||||
fetchedApp, err := appServer.Get(t.Context(), &application.ApplicationQuery{
|
||||
Name: &testApp.Name,
|
||||
Refresh: ptr.To(string(v1alpha1.RefreshTypeHard)),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
assert.Equal(t, int32(1), atomic.LoadInt32(&patched))
|
||||
case <-time.After(10 * time.Second):
|
||||
assert.Fail(t, "Out of time ( 10 seconds )")
|
||||
}
|
||||
assert.Equal(t, health.HealthStatusDegraded, fetchedApp.Status.Resources[0].Health.Status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInferResourcesStatusHealth(t *testing.T) {
|
||||
cacheClient := cache.NewCache(cache.NewInMemoryCache(1 * time.Hour))
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}))}
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -87,22 +87,15 @@ const ProgressiveSyncStatus = ({application}: {application: models.Application})
|
||||
);
|
||||
}}
|
||||
load={async () => {
|
||||
// Check if user has permission to read ApplicationSets
|
||||
const canReadApplicationSets = await services.accounts.canI('applicationsets', 'get', application.spec.project + '/' + application.metadata.name);
|
||||
|
||||
// Find ApplicationSet by searching all namespaces dynamically
|
||||
const appSetList = await services.applications.listApplicationSets();
|
||||
const appSet = appSetList.items?.find(item => item.metadata.name === appSetRef.name);
|
||||
|
||||
if (!appSet) {
|
||||
throw new Error(`ApplicationSet ${appSetRef.name} not found in any namespace`);
|
||||
}
|
||||
|
||||
return {canReadApplicationSets, appSet};
|
||||
return {appSet};
|
||||
}}>
|
||||
{({canReadApplicationSets, appSet}: {canReadApplicationSets: boolean; appSet: models.ApplicationSet}) => {
|
||||
{({appSet}: {appSet: models.ApplicationSet}) => {
|
||||
// Hide panel if: Progressive Sync disabled, no permission, or not RollingSync strategy
|
||||
if (!appSet.status?.applicationStatus || appSet?.spec?.strategy?.type !== 'RollingSync' || !canReadApplicationSets) {
|
||||
if (!appSet || !appSet.status?.applicationStatus || appSet?.spec?.strategy?.type !== 'RollingSync') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -349,7 +342,7 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh
|
||||
}}>
|
||||
{(data: models.ApplicationSyncWindowState) => (
|
||||
<React.Fragment>
|
||||
{data.assignedWindows && (
|
||||
{data?.assignedWindows && (
|
||||
<div className='application-status-panel__item' style={{position: 'relative'}}>
|
||||
{sectionLabel({
|
||||
title: 'SYNC WINDOWS',
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ApplicationsRefreshPanel = ({show, apps, hide}: {show: boolean; app
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ApplicationSelector apps={apps} formApi={formApi} />
|
||||
{show && <ApplicationSelector apps={apps} formApi={formApi} />}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
@@ -147,7 +147,7 @@ export const ApplicationsSyncPanel = ({show, apps, hide}: {show: boolean; apps:
|
||||
|
||||
<ApplicationRetryOptions id='applications-sync-panel' formApi={formApi} />
|
||||
|
||||
<ApplicationSelector apps={apps} formApi={formApi} />
|
||||
{show && <ApplicationSelector apps={apps} formApi={formApi} />}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
@@ -1496,13 +1496,13 @@ export const SyncWindowStatusIcon = ({state, window}: {state: appModels.SyncWind
|
||||
);
|
||||
};
|
||||
|
||||
export const ApplicationSyncWindowStatusIcon = ({project, state}: {project: string; state: appModels.ApplicationSyncWindowState}) => {
|
||||
export const ApplicationSyncWindowStatusIcon = ({project, state}: {project: string; state?: appModels.ApplicationSyncWindowState}) => {
|
||||
let className = '';
|
||||
let color = '';
|
||||
let deny = false;
|
||||
let allow = false;
|
||||
let inactiveAllow = false;
|
||||
if (state.assignedWindows !== undefined && state.assignedWindows.length > 0) {
|
||||
if (state?.assignedWindows !== undefined && state?.assignedWindows.length > 0) {
|
||||
if (state.activeWindows !== undefined && state.activeWindows.length > 0) {
|
||||
for (const w of state.activeWindows) {
|
||||
if (w.kind === 'deny') {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
12
ui/yarn.lock
12
ui/yarn.lock
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -34,9 +34,9 @@ func (s *secretsRepositoryBackend) CreateRepository(ctx context.Context, reposit
|
||||
},
|
||||
}
|
||||
|
||||
s.repositoryToSecret(repository, repositorySecret)
|
||||
updatedSecret := s.repositoryToSecret(repository, repositorySecret)
|
||||
|
||||
_, err := s.db.createSecret(ctx, repositorySecret)
|
||||
_, err := s.db.createSecret(ctx, updatedSecret)
|
||||
if err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
hasLabel, err := s.hasRepoTypeLabel(secName)
|
||||
@@ -142,9 +142,9 @@ func (s *secretsRepositoryBackend) UpdateRepository(ctx context.Context, reposit
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.repositoryToSecret(repository, repositorySecret)
|
||||
updatedSecret := s.repositoryToSecret(repository, repositorySecret)
|
||||
|
||||
_, err = s.db.kubeclientset.CoreV1().Secrets(s.db.ns).Update(ctx, repositorySecret, metav1.UpdateOptions{})
|
||||
_, err = s.db.kubeclientset.CoreV1().Secrets(s.db.ns).Update(ctx, updatedSecret, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -187,9 +187,9 @@ func (s *secretsRepositoryBackend) CreateRepoCreds(ctx context.Context, repoCred
|
||||
},
|
||||
}
|
||||
|
||||
repoCredsToSecret(repoCreds, repoCredsSecret)
|
||||
updatedSecret := repoCredsToSecret(repoCreds, repoCredsSecret)
|
||||
|
||||
_, err := s.db.createSecret(ctx, repoCredsSecret)
|
||||
_, err := s.db.createSecret(ctx, updatedSecret)
|
||||
if err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
return nil, status.Errorf(codes.AlreadyExists, "repository credentials %q already exists", repoCreds.URL)
|
||||
@@ -237,9 +237,9 @@ func (s *secretsRepositoryBackend) UpdateRepoCreds(ctx context.Context, repoCred
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoCredsToSecret(repoCreds, repoCredsSecret)
|
||||
updatedSecret := repoCredsToSecret(repoCreds, repoCredsSecret)
|
||||
|
||||
repoCredsSecret, err = s.db.kubeclientset.CoreV1().Secrets(s.db.ns).Update(ctx, repoCredsSecret, metav1.UpdateOptions{})
|
||||
repoCredsSecret, err = s.db.kubeclientset.CoreV1().Secrets(s.db.ns).Update(ctx, updatedSecret, metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -323,73 +323,75 @@ func (s *secretsRepositoryBackend) GetAllOCIRepoCreds(_ context.Context) ([]*app
|
||||
}
|
||||
|
||||
func secretToRepository(secret *corev1.Secret) (*appsv1.Repository, error) {
|
||||
secretCopy := secret.DeepCopy()
|
||||
|
||||
repository := &appsv1.Repository{
|
||||
Name: string(secret.Data["name"]),
|
||||
Repo: string(secret.Data["url"]),
|
||||
Username: string(secret.Data["username"]),
|
||||
Password: string(secret.Data["password"]),
|
||||
BearerToken: string(secret.Data["bearerToken"]),
|
||||
SSHPrivateKey: string(secret.Data["sshPrivateKey"]),
|
||||
TLSClientCertData: string(secret.Data["tlsClientCertData"]),
|
||||
TLSClientCertKey: string(secret.Data["tlsClientCertKey"]),
|
||||
Type: string(secret.Data["type"]),
|
||||
GithubAppPrivateKey: string(secret.Data["githubAppPrivateKey"]),
|
||||
GitHubAppEnterpriseBaseURL: string(secret.Data["githubAppEnterpriseBaseUrl"]),
|
||||
Proxy: string(secret.Data["proxy"]),
|
||||
NoProxy: string(secret.Data["noProxy"]),
|
||||
Project: string(secret.Data["project"]),
|
||||
GCPServiceAccountKey: string(secret.Data["gcpServiceAccountKey"]),
|
||||
Name: string(secretCopy.Data["name"]),
|
||||
Repo: string(secretCopy.Data["url"]),
|
||||
Username: string(secretCopy.Data["username"]),
|
||||
Password: string(secretCopy.Data["password"]),
|
||||
BearerToken: string(secretCopy.Data["bearerToken"]),
|
||||
SSHPrivateKey: string(secretCopy.Data["sshPrivateKey"]),
|
||||
TLSClientCertData: string(secretCopy.Data["tlsClientCertData"]),
|
||||
TLSClientCertKey: string(secretCopy.Data["tlsClientCertKey"]),
|
||||
Type: string(secretCopy.Data["type"]),
|
||||
GithubAppPrivateKey: string(secretCopy.Data["githubAppPrivateKey"]),
|
||||
GitHubAppEnterpriseBaseURL: string(secretCopy.Data["githubAppEnterpriseBaseUrl"]),
|
||||
Proxy: string(secretCopy.Data["proxy"]),
|
||||
NoProxy: string(secretCopy.Data["noProxy"]),
|
||||
Project: string(secretCopy.Data["project"]),
|
||||
GCPServiceAccountKey: string(secretCopy.Data["gcpServiceAccountKey"]),
|
||||
}
|
||||
|
||||
insecureIgnoreHostKey, err := boolOrFalse(secret, "insecureIgnoreHostKey")
|
||||
insecureIgnoreHostKey, err := boolOrFalse(secretCopy, "insecureIgnoreHostKey")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.InsecureIgnoreHostKey = insecureIgnoreHostKey
|
||||
|
||||
insecure, err := boolOrFalse(secret, "insecure")
|
||||
insecure, err := boolOrFalse(secretCopy, "insecure")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.Insecure = insecure
|
||||
|
||||
enableLfs, err := boolOrFalse(secret, "enableLfs")
|
||||
enableLfs, err := boolOrFalse(secretCopy, "enableLfs")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.EnableLFS = enableLfs
|
||||
|
||||
enableOCI, err := boolOrFalse(secret, "enableOCI")
|
||||
enableOCI, err := boolOrFalse(secretCopy, "enableOCI")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.EnableOCI = enableOCI
|
||||
|
||||
insecureOCIForceHTTP, err := boolOrFalse(secret, "insecureOCIForceHttp")
|
||||
insecureOCIForceHTTP, err := boolOrFalse(secretCopy, "insecureOCIForceHttp")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.InsecureOCIForceHttp = insecureOCIForceHTTP
|
||||
|
||||
githubAppID, err := intOrZero(secret, "githubAppID")
|
||||
githubAppID, err := intOrZero(secretCopy, "githubAppID")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.GithubAppId = githubAppID
|
||||
|
||||
githubAppInstallationID, err := intOrZero(secret, "githubAppInstallationID")
|
||||
githubAppInstallationID, err := intOrZero(secretCopy, "githubAppInstallationID")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.GithubAppInstallationId = githubAppInstallationID
|
||||
|
||||
forceBasicAuth, err := boolOrFalse(secret, "forceHttpBasicAuth")
|
||||
forceBasicAuth, err := boolOrFalse(secretCopy, "forceHttpBasicAuth")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.ForceHttpBasicAuth = forceBasicAuth
|
||||
|
||||
useAzureWorkloadIdentity, err := boolOrFalse(secret, "useAzureWorkloadIdentity")
|
||||
useAzureWorkloadIdentity, err := boolOrFalse(secretCopy, "useAzureWorkloadIdentity")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
@@ -398,86 +400,92 @@ func secretToRepository(secret *corev1.Secret) (*appsv1.Repository, error) {
|
||||
return repository, nil
|
||||
}
|
||||
|
||||
func (s *secretsRepositoryBackend) repositoryToSecret(repository *appsv1.Repository, secret *corev1.Secret) {
|
||||
if secret.Data == nil {
|
||||
secret.Data = make(map[string][]byte)
|
||||
func (s *secretsRepositoryBackend) repositoryToSecret(repository *appsv1.Repository, secret *corev1.Secret) *corev1.Secret {
|
||||
secretCopy := secret.DeepCopy()
|
||||
|
||||
if secretCopy.Data == nil {
|
||||
secretCopy.Data = make(map[string][]byte)
|
||||
}
|
||||
|
||||
updateSecretString(secret, "name", repository.Name)
|
||||
updateSecretString(secret, "project", repository.Project)
|
||||
updateSecretString(secret, "url", repository.Repo)
|
||||
updateSecretString(secret, "username", repository.Username)
|
||||
updateSecretString(secret, "password", repository.Password)
|
||||
updateSecretString(secret, "bearerToken", repository.BearerToken)
|
||||
updateSecretString(secret, "sshPrivateKey", repository.SSHPrivateKey)
|
||||
updateSecretBool(secret, "enableOCI", repository.EnableOCI)
|
||||
updateSecretBool(secret, "insecureOCIForceHttp", repository.InsecureOCIForceHttp)
|
||||
updateSecretString(secret, "tlsClientCertData", repository.TLSClientCertData)
|
||||
updateSecretString(secret, "tlsClientCertKey", repository.TLSClientCertKey)
|
||||
updateSecretString(secret, "type", repository.Type)
|
||||
updateSecretString(secret, "githubAppPrivateKey", repository.GithubAppPrivateKey)
|
||||
updateSecretInt(secret, "githubAppID", repository.GithubAppId)
|
||||
updateSecretInt(secret, "githubAppInstallationID", repository.GithubAppInstallationId)
|
||||
updateSecretString(secret, "githubAppEnterpriseBaseUrl", repository.GitHubAppEnterpriseBaseURL)
|
||||
updateSecretBool(secret, "insecureIgnoreHostKey", repository.InsecureIgnoreHostKey)
|
||||
updateSecretBool(secret, "insecure", repository.Insecure)
|
||||
updateSecretBool(secret, "enableLfs", repository.EnableLFS)
|
||||
updateSecretString(secret, "proxy", repository.Proxy)
|
||||
updateSecretString(secret, "noProxy", repository.NoProxy)
|
||||
updateSecretString(secret, "gcpServiceAccountKey", repository.GCPServiceAccountKey)
|
||||
updateSecretBool(secret, "forceHttpBasicAuth", repository.ForceHttpBasicAuth)
|
||||
updateSecretBool(secret, "useAzureWorkloadIdentity", repository.UseAzureWorkloadIdentity)
|
||||
addSecretMetadata(secret, s.getSecretType())
|
||||
updateSecretString(secretCopy, "name", repository.Name)
|
||||
updateSecretString(secretCopy, "project", repository.Project)
|
||||
updateSecretString(secretCopy, "url", repository.Repo)
|
||||
updateSecretString(secretCopy, "username", repository.Username)
|
||||
updateSecretString(secretCopy, "password", repository.Password)
|
||||
updateSecretString(secretCopy, "bearerToken", repository.BearerToken)
|
||||
updateSecretString(secretCopy, "sshPrivateKey", repository.SSHPrivateKey)
|
||||
updateSecretBool(secretCopy, "enableOCI", repository.EnableOCI)
|
||||
updateSecretBool(secretCopy, "insecureOCIForceHttp", repository.InsecureOCIForceHttp)
|
||||
updateSecretString(secretCopy, "tlsClientCertData", repository.TLSClientCertData)
|
||||
updateSecretString(secretCopy, "tlsClientCertKey", repository.TLSClientCertKey)
|
||||
updateSecretString(secretCopy, "type", repository.Type)
|
||||
updateSecretString(secretCopy, "githubAppPrivateKey", repository.GithubAppPrivateKey)
|
||||
updateSecretInt(secretCopy, "githubAppID", repository.GithubAppId)
|
||||
updateSecretInt(secretCopy, "githubAppInstallationID", repository.GithubAppInstallationId)
|
||||
updateSecretString(secretCopy, "githubAppEnterpriseBaseUrl", repository.GitHubAppEnterpriseBaseURL)
|
||||
updateSecretBool(secretCopy, "insecureIgnoreHostKey", repository.InsecureIgnoreHostKey)
|
||||
updateSecretBool(secretCopy, "insecure", repository.Insecure)
|
||||
updateSecretBool(secretCopy, "enableLfs", repository.EnableLFS)
|
||||
updateSecretString(secretCopy, "proxy", repository.Proxy)
|
||||
updateSecretString(secretCopy, "noProxy", repository.NoProxy)
|
||||
updateSecretString(secretCopy, "gcpServiceAccountKey", repository.GCPServiceAccountKey)
|
||||
updateSecretBool(secretCopy, "forceHttpBasicAuth", repository.ForceHttpBasicAuth)
|
||||
updateSecretBool(secretCopy, "useAzureWorkloadIdentity", repository.UseAzureWorkloadIdentity)
|
||||
addSecretMetadata(secretCopy, s.getSecretType())
|
||||
|
||||
return secretCopy
|
||||
}
|
||||
|
||||
func (s *secretsRepositoryBackend) secretToRepoCred(secret *corev1.Secret) (*appsv1.RepoCreds, error) {
|
||||
secretCopy := secret.DeepCopy()
|
||||
|
||||
repository := &appsv1.RepoCreds{
|
||||
URL: string(secret.Data["url"]),
|
||||
Username: string(secret.Data["username"]),
|
||||
Password: string(secret.Data["password"]),
|
||||
BearerToken: string(secret.Data["bearerToken"]),
|
||||
SSHPrivateKey: string(secret.Data["sshPrivateKey"]),
|
||||
TLSClientCertData: string(secret.Data["tlsClientCertData"]),
|
||||
TLSClientCertKey: string(secret.Data["tlsClientCertKey"]),
|
||||
Type: string(secret.Data["type"]),
|
||||
GithubAppPrivateKey: string(secret.Data["githubAppPrivateKey"]),
|
||||
GitHubAppEnterpriseBaseURL: string(secret.Data["githubAppEnterpriseBaseUrl"]),
|
||||
GCPServiceAccountKey: string(secret.Data["gcpServiceAccountKey"]),
|
||||
Proxy: string(secret.Data["proxy"]),
|
||||
NoProxy: string(secret.Data["noProxy"]),
|
||||
URL: string(secretCopy.Data["url"]),
|
||||
Username: string(secretCopy.Data["username"]),
|
||||
Password: string(secretCopy.Data["password"]),
|
||||
BearerToken: string(secretCopy.Data["bearerToken"]),
|
||||
SSHPrivateKey: string(secretCopy.Data["sshPrivateKey"]),
|
||||
TLSClientCertData: string(secretCopy.Data["tlsClientCertData"]),
|
||||
TLSClientCertKey: string(secretCopy.Data["tlsClientCertKey"]),
|
||||
Type: string(secretCopy.Data["type"]),
|
||||
GithubAppPrivateKey: string(secretCopy.Data["githubAppPrivateKey"]),
|
||||
GitHubAppEnterpriseBaseURL: string(secretCopy.Data["githubAppEnterpriseBaseUrl"]),
|
||||
GCPServiceAccountKey: string(secretCopy.Data["gcpServiceAccountKey"]),
|
||||
Proxy: string(secretCopy.Data["proxy"]),
|
||||
NoProxy: string(secretCopy.Data["noProxy"]),
|
||||
}
|
||||
|
||||
enableOCI, err := boolOrFalse(secret, "enableOCI")
|
||||
enableOCI, err := boolOrFalse(secretCopy, "enableOCI")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.EnableOCI = enableOCI
|
||||
|
||||
insecureOCIForceHTTP, err := boolOrFalse(secret, "insecureOCIForceHttp")
|
||||
insecureOCIForceHTTP, err := boolOrFalse(secretCopy, "insecureOCIForceHttp")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.InsecureOCIForceHttp = insecureOCIForceHTTP
|
||||
|
||||
githubAppID, err := intOrZero(secret, "githubAppID")
|
||||
githubAppID, err := intOrZero(secretCopy, "githubAppID")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.GithubAppId = githubAppID
|
||||
|
||||
githubAppInstallationID, err := intOrZero(secret, "githubAppInstallationID")
|
||||
githubAppInstallationID, err := intOrZero(secretCopy, "githubAppInstallationID")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.GithubAppInstallationId = githubAppInstallationID
|
||||
|
||||
forceBasicAuth, err := boolOrFalse(secret, "forceHttpBasicAuth")
|
||||
forceBasicAuth, err := boolOrFalse(secretCopy, "forceHttpBasicAuth")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
repository.ForceHttpBasicAuth = forceBasicAuth
|
||||
|
||||
useAzureWorkloadIdentity, err := boolOrFalse(secret, "useAzureWorkloadIdentity")
|
||||
useAzureWorkloadIdentity, err := boolOrFalse(secretCopy, "useAzureWorkloadIdentity")
|
||||
if err != nil {
|
||||
return repository, err
|
||||
}
|
||||
@@ -486,31 +494,35 @@ func (s *secretsRepositoryBackend) secretToRepoCred(secret *corev1.Secret) (*app
|
||||
return repository, nil
|
||||
}
|
||||
|
||||
func repoCredsToSecret(repoCreds *appsv1.RepoCreds, secret *corev1.Secret) {
|
||||
if secret.Data == nil {
|
||||
secret.Data = make(map[string][]byte)
|
||||
func repoCredsToSecret(repoCreds *appsv1.RepoCreds, secret *corev1.Secret) *corev1.Secret {
|
||||
secretCopy := secret.DeepCopy()
|
||||
|
||||
if secretCopy.Data == nil {
|
||||
secretCopy.Data = make(map[string][]byte)
|
||||
}
|
||||
|
||||
updateSecretString(secret, "url", repoCreds.URL)
|
||||
updateSecretString(secret, "username", repoCreds.Username)
|
||||
updateSecretString(secret, "password", repoCreds.Password)
|
||||
updateSecretString(secret, "bearerToken", repoCreds.BearerToken)
|
||||
updateSecretString(secret, "sshPrivateKey", repoCreds.SSHPrivateKey)
|
||||
updateSecretBool(secret, "enableOCI", repoCreds.EnableOCI)
|
||||
updateSecretBool(secret, "insecureOCIForceHttp", repoCreds.InsecureOCIForceHttp)
|
||||
updateSecretString(secret, "tlsClientCertData", repoCreds.TLSClientCertData)
|
||||
updateSecretString(secret, "tlsClientCertKey", repoCreds.TLSClientCertKey)
|
||||
updateSecretString(secret, "type", repoCreds.Type)
|
||||
updateSecretString(secret, "githubAppPrivateKey", repoCreds.GithubAppPrivateKey)
|
||||
updateSecretInt(secret, "githubAppID", repoCreds.GithubAppId)
|
||||
updateSecretInt(secret, "githubAppInstallationID", repoCreds.GithubAppInstallationId)
|
||||
updateSecretString(secret, "githubAppEnterpriseBaseUrl", repoCreds.GitHubAppEnterpriseBaseURL)
|
||||
updateSecretString(secret, "gcpServiceAccountKey", repoCreds.GCPServiceAccountKey)
|
||||
updateSecretString(secret, "proxy", repoCreds.Proxy)
|
||||
updateSecretString(secret, "noProxy", repoCreds.NoProxy)
|
||||
updateSecretBool(secret, "forceHttpBasicAuth", repoCreds.ForceHttpBasicAuth)
|
||||
updateSecretBool(secret, "useAzureWorkloadIdentity", repoCreds.UseAzureWorkloadIdentity)
|
||||
addSecretMetadata(secret, common.LabelValueSecretTypeRepoCreds)
|
||||
updateSecretString(secretCopy, "url", repoCreds.URL)
|
||||
updateSecretString(secretCopy, "username", repoCreds.Username)
|
||||
updateSecretString(secretCopy, "password", repoCreds.Password)
|
||||
updateSecretString(secretCopy, "bearerToken", repoCreds.BearerToken)
|
||||
updateSecretString(secretCopy, "sshPrivateKey", repoCreds.SSHPrivateKey)
|
||||
updateSecretBool(secretCopy, "enableOCI", repoCreds.EnableOCI)
|
||||
updateSecretBool(secretCopy, "insecureOCIForceHttp", repoCreds.InsecureOCIForceHttp)
|
||||
updateSecretString(secretCopy, "tlsClientCertData", repoCreds.TLSClientCertData)
|
||||
updateSecretString(secretCopy, "tlsClientCertKey", repoCreds.TLSClientCertKey)
|
||||
updateSecretString(secretCopy, "type", repoCreds.Type)
|
||||
updateSecretString(secretCopy, "githubAppPrivateKey", repoCreds.GithubAppPrivateKey)
|
||||
updateSecretInt(secretCopy, "githubAppID", repoCreds.GithubAppId)
|
||||
updateSecretInt(secretCopy, "githubAppInstallationID", repoCreds.GithubAppInstallationId)
|
||||
updateSecretString(secretCopy, "githubAppEnterpriseBaseUrl", repoCreds.GitHubAppEnterpriseBaseURL)
|
||||
updateSecretString(secretCopy, "gcpServiceAccountKey", repoCreds.GCPServiceAccountKey)
|
||||
updateSecretString(secretCopy, "proxy", repoCreds.Proxy)
|
||||
updateSecretString(secretCopy, "noProxy", repoCreds.NoProxy)
|
||||
updateSecretBool(secretCopy, "forceHttpBasicAuth", repoCreds.ForceHttpBasicAuth)
|
||||
updateSecretBool(secretCopy, "useAzureWorkloadIdentity", repoCreds.UseAzureWorkloadIdentity)
|
||||
addSecretMetadata(secretCopy, common.LabelValueSecretTypeRepoCreds)
|
||||
|
||||
return secretCopy
|
||||
}
|
||||
|
||||
func (s *secretsRepositoryBackend) getRepositorySecret(repoURL, project string, allowFallback bool) (*corev1.Secret, error) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -85,9 +87,9 @@ func TestSecretsRepositoryBackend_CreateRepository(t *testing.T) {
|
||||
t.Parallel()
|
||||
secret := &corev1.Secret{}
|
||||
s := secretsRepositoryBackend{}
|
||||
s.repositoryToSecret(repo, secret)
|
||||
delete(secret.Labels, common.LabelKeySecretType)
|
||||
f := setupWithK8sObjects(secret)
|
||||
updatedSecret := s.repositoryToSecret(repo, secret)
|
||||
delete(updatedSecret.Labels, common.LabelKeySecretType)
|
||||
f := setupWithK8sObjects(updatedSecret)
|
||||
f.clientSet.ReactionChain = nil
|
||||
f.clientSet.AddReactor("create", "secrets", func(_ k8stesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
gr := schema.GroupResource{
|
||||
@@ -122,8 +124,8 @@ func TestSecretsRepositoryBackend_CreateRepository(t *testing.T) {
|
||||
},
|
||||
}
|
||||
s := secretsRepositoryBackend{}
|
||||
s.repositoryToSecret(repo, secret)
|
||||
f := setupWithK8sObjects(secret)
|
||||
updatedSecret := s.repositoryToSecret(repo, secret)
|
||||
f := setupWithK8sObjects(updatedSecret)
|
||||
f.clientSet.ReactionChain = nil
|
||||
f.clientSet.WatchReactionChain = nil
|
||||
f.clientSet.AddReactor("create", "secrets", func(_ k8stesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
@@ -134,7 +136,7 @@ func TestSecretsRepositoryBackend_CreateRepository(t *testing.T) {
|
||||
return true, nil, apierrors.NewAlreadyExists(gr, "already exists")
|
||||
})
|
||||
watcher := watch.NewFakeWithChanSize(1, true)
|
||||
watcher.Add(secret)
|
||||
watcher.Add(updatedSecret)
|
||||
f.clientSet.AddWatchReactor("secrets", func(_ k8stesting.Action) (handled bool, ret watch.Interface, err error) {
|
||||
return true, watcher, nil
|
||||
})
|
||||
@@ -946,7 +948,7 @@ func TestRepoCredsToSecret(t *testing.T) {
|
||||
GithubAppInstallationId: 456,
|
||||
GitHubAppEnterpriseBaseURL: "GitHubAppEnterpriseBaseURL",
|
||||
}
|
||||
repoCredsToSecret(creds, s)
|
||||
s = repoCredsToSecret(creds, s)
|
||||
assert.Equal(t, []byte(creds.URL), s.Data["url"])
|
||||
assert.Equal(t, []byte(creds.Username), s.Data["username"])
|
||||
assert.Equal(t, []byte(creds.Password), s.Data["password"])
|
||||
@@ -962,3 +964,169 @@ func TestRepoCredsToSecret(t *testing.T) {
|
||||
assert.Equal(t, map[string]string{common.AnnotationKeyManagedBy: common.AnnotationValueManagedByArgoCD}, s.Annotations)
|
||||
assert.Equal(t, map[string]string{common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds}, s.Labels)
|
||||
}
|
||||
|
||||
func TestRaceConditionInRepoCredsOperations(t *testing.T) {
|
||||
// Create a single shared secret that will be accessed concurrently
|
||||
sharedSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj/argo-cd.git", ""),
|
||||
Namespace: testNamespace,
|
||||
Labels: map[string]string{
|
||||
common.LabelKeySecretType: common.LabelValueSecretTypeRepoCreds,
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"url": []byte("git@github.com:argoproj/argo-cd.git"),
|
||||
"username": []byte("test-user"),
|
||||
"password": []byte("test-pass"),
|
||||
},
|
||||
}
|
||||
|
||||
// Create test credentials that we'll use for conversion
|
||||
repoCreds := &appsv1.RepoCreds{
|
||||
URL: "git@github.com:argoproj/argo-cd.git",
|
||||
Username: "test-user",
|
||||
Password: "test-pass",
|
||||
}
|
||||
|
||||
backend := &secretsRepositoryBackend{}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
concurrentOps := 50
|
||||
errChan := make(chan error, concurrentOps*2) // Channel to collect errors
|
||||
|
||||
// Launch goroutines that perform concurrent operations
|
||||
for i := 0; i < concurrentOps; i++ {
|
||||
wg.Add(2)
|
||||
|
||||
// One goroutine converts from RepoCreds to Secret
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("panic in repoCredsToSecret: %v", r)
|
||||
}
|
||||
}()
|
||||
_ = repoCredsToSecret(repoCreds, sharedSecret)
|
||||
}()
|
||||
|
||||
// Another goroutine converts from Secret to RepoCreds
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("panic in secretToRepoCred: %v", r)
|
||||
}
|
||||
}()
|
||||
creds, err := backend.secretToRepoCred(sharedSecret)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("error in secretToRepoCred: %w", err)
|
||||
return
|
||||
}
|
||||
// Verify data integrity
|
||||
if creds.URL != repoCreds.URL || creds.Username != repoCreds.Username || creds.Password != repoCreds.Password {
|
||||
errChan <- fmt.Errorf("data mismatch in conversion: expected %v, got %v", repoCreds, creds)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
// Check for any errors that occurred during concurrent operations
|
||||
for err := range errChan {
|
||||
t.Errorf("concurrent operation error: %v", err)
|
||||
}
|
||||
|
||||
// Verify final state
|
||||
finalCreds, err := backend.secretToRepoCred(sharedSecret)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, repoCreds.URL, finalCreds.URL)
|
||||
assert.Equal(t, repoCreds.Username, finalCreds.Username)
|
||||
assert.Equal(t, repoCreds.Password, finalCreds.Password)
|
||||
}
|
||||
|
||||
func TestRaceConditionInRepositoryOperations(t *testing.T) {
|
||||
// Create a single shared secret that will be accessed concurrently
|
||||
sharedSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: RepoURLToSecretName(repoSecretPrefix, "git@github.com:argoproj/argo-cd.git", ""),
|
||||
Namespace: testNamespace,
|
||||
Labels: map[string]string{
|
||||
common.LabelKeySecretType: common.LabelValueSecretTypeRepository,
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"url": []byte("git@github.com:argoproj/argo-cd.git"),
|
||||
"name": []byte("test-repo"),
|
||||
"username": []byte("test-user"),
|
||||
"password": []byte("test-pass"),
|
||||
},
|
||||
}
|
||||
|
||||
// Create test repository that we'll use for conversion
|
||||
repo := &appsv1.Repository{
|
||||
Name: "test-repo",
|
||||
Repo: "git@github.com:argoproj/argo-cd.git",
|
||||
Username: "test-user",
|
||||
Password: "test-pass",
|
||||
}
|
||||
|
||||
backend := &secretsRepositoryBackend{}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
concurrentOps := 50
|
||||
errChan := make(chan error, concurrentOps*2) // Channel to collect errors
|
||||
|
||||
// Launch goroutines that perform concurrent operations
|
||||
for i := 0; i < concurrentOps; i++ {
|
||||
wg.Add(2)
|
||||
|
||||
// One goroutine converts from Repository to Secret
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("panic in repositoryToSecret: %v", r)
|
||||
}
|
||||
}()
|
||||
_ = backend.repositoryToSecret(repo, sharedSecret)
|
||||
}()
|
||||
|
||||
// Another goroutine converts from Secret to Repository
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("panic in secretToRepository: %v", r)
|
||||
}
|
||||
}()
|
||||
repository, err := secretToRepository(sharedSecret)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("error in secretToRepository: %w", err)
|
||||
return
|
||||
}
|
||||
// Verify data integrity
|
||||
if repository.Name != repo.Name || repository.Repo != repo.Repo ||
|
||||
repository.Username != repo.Username || repository.Password != repo.Password {
|
||||
errChan <- fmt.Errorf("data mismatch in conversion: expected %v, got %v", repo, repository)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
// Check for any errors that occurred during concurrent operations
|
||||
for err := range errChan {
|
||||
t.Errorf("concurrent operation error: %v", err)
|
||||
}
|
||||
|
||||
// Verify final state
|
||||
finalRepo, err := secretToRepository(sharedSecret)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, repo.Name, finalRepo.Name)
|
||||
assert.Equal(t, repo.Repo, finalRepo.Repo)
|
||||
assert.Equal(t, repo.Username, finalRepo.Username)
|
||||
assert.Equal(t, repo.Password, finalRepo.Password)
|
||||
}
|
||||
|
||||
11
util/env/env.go
vendored
11
util/env/env.go
vendored
@@ -7,6 +7,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
timeutil "github.com/argoproj/pkg/time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -125,8 +127,13 @@ func ParseDurationFromEnv(env string, defaultValue, minimum, maximum time.Durati
|
||||
}
|
||||
dur, err := time.ParseDuration(str)
|
||||
if err != nil {
|
||||
log.Warnf("Could not parse '%s' as a duration string from environment %s", str, env)
|
||||
return defaultValue
|
||||
// provides backwards compatibility for durations defined in days, see: https://github.com/argoproj/argo-cd/issues/24740
|
||||
durPtr, err2 := timeutil.ParseDuration(str)
|
||||
if err2 != nil {
|
||||
log.Warnf("Could not parse '%s' as a duration from environment %s", str, env)
|
||||
return defaultValue
|
||||
}
|
||||
dur = *durPtr
|
||||
}
|
||||
|
||||
if dur < minimum {
|
||||
|
||||
84
util/env/env_test.go
vendored
84
util/env/env_test.go
vendored
@@ -142,6 +142,90 @@ func TestParseDurationFromEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDurationFromEnvEdgeCases(t *testing.T) {
|
||||
envKey := "SOME_ENV_KEY"
|
||||
def := 3 * time.Minute
|
||||
minimum := 1 * time.Second
|
||||
maximum := 2160 * time.Hour // 3 months
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
env string
|
||||
expected time.Duration
|
||||
}{{
|
||||
name: "EnvNotSet",
|
||||
expected: def,
|
||||
}, {
|
||||
name: "Durations defined as days are valid",
|
||||
env: "12d",
|
||||
expected: time.Hour * 24 * 12,
|
||||
}, {
|
||||
name: "Negative durations should fail parsing and use the default value",
|
||||
env: "-1h",
|
||||
expected: def,
|
||||
}, {
|
||||
name: "Negative day durations should fail parsing and use the default value",
|
||||
env: "-12d",
|
||||
expected: def,
|
||||
}, {
|
||||
name: "Scientific notation should fail parsing and use the default value",
|
||||
env: "1e3s",
|
||||
expected: def,
|
||||
}, {
|
||||
name: "Durations with a leading zero are considered valid and parsed as decimal notation",
|
||||
env: "0755s",
|
||||
expected: time.Second * 755,
|
||||
}, {
|
||||
name: "Durations with many leading zeroes are considered valid and parsed as decimal notation",
|
||||
env: "000083m",
|
||||
expected: time.Minute * 83,
|
||||
}, {
|
||||
name: "Decimal Durations should not fail parsing",
|
||||
env: "30.5m",
|
||||
expected: time.Minute*30 + time.Second*30,
|
||||
}, {
|
||||
name: "Decimal Day Durations should fail parsing and use the default value",
|
||||
env: "30.5d",
|
||||
expected: def,
|
||||
}, {
|
||||
name: "Fraction Durations should fail parsing and use the default value",
|
||||
env: "1/2h",
|
||||
expected: def,
|
||||
}, {
|
||||
name: "Durations without a time unit should fail parsing and use the default value",
|
||||
env: "15",
|
||||
expected: def,
|
||||
}, {
|
||||
name: "Durations with a trailing symbol should fail parsing and use the default value",
|
||||
env: "+12d",
|
||||
expected: def,
|
||||
}, {
|
||||
name: "Leading space Duration should fail parsing use the default value",
|
||||
env: " 2h",
|
||||
expected: def,
|
||||
}, {
|
||||
name: "Trailing space Duration should fail parsing use the default value",
|
||||
env: "6m ",
|
||||
expected: def,
|
||||
}, {
|
||||
name: "Empty Duration should fail parsing use the default value",
|
||||
env: "",
|
||||
expected: def,
|
||||
}, {
|
||||
name: "Whitespace Duration should fail parsing and use the default value",
|
||||
env: " ",
|
||||
expected: def,
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv(envKey, tc.env)
|
||||
val := ParseDurationFromEnv(envKey, def, minimum, maximum)
|
||||
assert.Equal(t, tc.expected, val)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ParseBoolFromEnv(t *testing.T) {
|
||||
envKey := "SOMEKEY"
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ func RunWithRedactor(cmd *exec.Cmd, redactor func(text string) string) (string,
|
||||
}
|
||||
|
||||
func RunWithExecRunOpts(cmd *exec.Cmd, opts ExecRunOpts) (string, error) {
|
||||
cmdOpts := CmdOpts{Timeout: timeout, FatalTimeout: fatalTimeout, Redactor: opts.Redactor, TimeoutBehavior: opts.TimeoutBehavior, SkipErrorLogging: opts.SkipErrorLogging}
|
||||
cmdOpts := CmdOpts{Timeout: timeout, FatalTimeout: fatalTimeout, Redactor: opts.Redactor, TimeoutBehavior: opts.TimeoutBehavior, SkipErrorLogging: opts.SkipErrorLogging, CaptureStderr: opts.CaptureStderr}
|
||||
span := tracing.NewLoggingTracer(log.NewLogrusLogger(log.NewWithCurrentConfig())).StartSpan(fmt.Sprintf("exec %v", cmd.Args[0]))
|
||||
span.SetBaggageItem("dir", cmd.Dir)
|
||||
if cmdOpts.Redactor != nil {
|
||||
|
||||
@@ -217,3 +217,11 @@ func TestRunCaptureStderr(t *testing.T) {
|
||||
assert.Equal(t, "hello world\nmy-error", output)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRunWithExecRunOptsCaptureStderr(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
cmd := exec.CommandContext(ctx, "sh", "-c", "echo hello world && echo my-error >&2 && exit 0")
|
||||
output, err := RunWithExecRunOpts(cmd, ExecRunOpts{CaptureStderr: true})
|
||||
assert.Equal(t, "hello world\nmy-error", output)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
20
util/guard/guard.go
Normal file
20
util/guard/guard.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package guard
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// Minimal logger contract; avoids depending on any specific logging package.
|
||||
type Logger interface{ Errorf(string, ...any) }
|
||||
|
||||
// Run executes fn and recovers a panic, logging a component-specific message.
|
||||
func RecoverAndLog(fn func(), log Logger, msg string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if log != nil {
|
||||
log.Errorf("%s: %v %s", msg, r, debug.Stack())
|
||||
}
|
||||
}
|
||||
}()
|
||||
fn()
|
||||
}
|
||||
92
util/guard/guard_test.go
Normal file
92
util/guard/guard_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package guard
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type nop struct{}
|
||||
|
||||
func (nop) Errorf(string, ...any) {}
|
||||
|
||||
// recorder is a thread-safe logger that captures formatted messages.
|
||||
type recorder struct {
|
||||
mu sync.Mutex
|
||||
calls int
|
||||
msgs []string
|
||||
}
|
||||
|
||||
func (r *recorder) Errorf(format string, args ...any) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.calls++
|
||||
r.msgs = append(r.msgs, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func TestRun_Recovers(_ *testing.T) {
|
||||
RecoverAndLog(func() { panic("boom") }, nop{}, "msg") // fails if panic escapes
|
||||
}
|
||||
|
||||
func TestRun_AllowsNextCall(t *testing.T) {
|
||||
ran := false
|
||||
RecoverAndLog(func() { panic("boom") }, nop{}, "msg")
|
||||
RecoverAndLog(func() { ran = true }, nop{}, "msg")
|
||||
if !ran {
|
||||
t.Fatal("expected second callback to run")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_LogsMessageAndStack(t *testing.T) {
|
||||
r := &recorder{}
|
||||
RecoverAndLog(func() { panic("boom") }, r, "msg")
|
||||
if r.calls != 1 {
|
||||
t.Fatalf("expected 1 log call, got %d", r.calls)
|
||||
}
|
||||
got := strings.Join(r.msgs, "\n")
|
||||
if !strings.Contains(got, "msg") {
|
||||
t.Errorf("expected log to contain message %q; got %q", "msg", got)
|
||||
}
|
||||
if !strings.Contains(got, "boom") {
|
||||
t.Errorf("expected log to contain panic value %q; got %q", "boom", got)
|
||||
}
|
||||
// Heuristic check that a stack trace was included.
|
||||
if !strings.Contains(got, "guard.go") && !strings.Contains(got, "runtime/panic.go") && !strings.Contains(got, "goroutine") {
|
||||
t.Errorf("expected log to contain a stack trace; got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_NilLoggerDoesNotPanic(_ *testing.T) {
|
||||
var l Logger // nil
|
||||
RecoverAndLog(func() { panic("boom") }, l, "ignored")
|
||||
}
|
||||
|
||||
func TestRun_NoPanicDoesNotLog(t *testing.T) {
|
||||
r := &recorder{}
|
||||
ran := false
|
||||
RecoverAndLog(func() { ran = true }, r, "msg")
|
||||
if !ran {
|
||||
t.Fatal("expected fn to run")
|
||||
}
|
||||
if r.calls != 0 {
|
||||
t.Fatalf("expected 0 log calls, got %d", r.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_ConcurrentPanicsLogged(t *testing.T) {
|
||||
r := &recorder{}
|
||||
const n = 10
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
RecoverAndLog(func() { panic(fmt.Sprintf("boom-%d", i)) }, r, "msg")
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
if r.calls != n {
|
||||
t.Fatalf("expected %d log calls, got %d", n, r.calls)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -19,5 +19,5 @@ func (c composableFS) Open(name string) (f fs.File, err error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
return f, err
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/v3/util/db"
|
||||
"github.com/argoproj/argo-cd/v3/util/git"
|
||||
"github.com/argoproj/argo-cd/v3/util/glob"
|
||||
"github.com/argoproj/argo-cd/v3/util/guard"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
)
|
||||
|
||||
@@ -52,6 +53,8 @@ const usernameRegex = `[\w\.][\w\.-]{0,30}[\w\.\$-]?`
|
||||
|
||||
const payloadQueueSize = 50000
|
||||
|
||||
const panicMsgServer = "panic while processing api-server webhook event"
|
||||
|
||||
var _ settingsSource = &settings.SettingsManager{}
|
||||
|
||||
type ArgoCDWebhookHandler struct {
|
||||
@@ -127,6 +130,7 @@ func NewHandler(namespace string, applicationNamespaces []string, webhookParalle
|
||||
}
|
||||
|
||||
func (a *ArgoCDWebhookHandler) startWorkerPool(webhookParallelism int) {
|
||||
compLog := log.WithField("component", "api-server-webhook")
|
||||
for i := 0; i < webhookParallelism; i++ {
|
||||
a.Add(1)
|
||||
go func() {
|
||||
@@ -136,7 +140,7 @@ func (a *ArgoCDWebhookHandler) startWorkerPool(webhookParallelism int) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
a.HandleEvent(payload)
|
||||
guard.RecoverAndLog(func() { a.HandleEvent(payload) }, compLog, panicMsgServer)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -154,10 +158,12 @@ func (a *ArgoCDWebhookHandler) affectedRevisionInfo(payloadIf any) (webURLs []st
|
||||
case azuredevops.GitPushEvent:
|
||||
// See: https://learn.microsoft.com/en-us/azure/devops/service-hooks/events?view=azure-devops#git.push
|
||||
webURLs = append(webURLs, payload.Resource.Repository.RemoteURL)
|
||||
revision = ParseRevision(payload.Resource.RefUpdates[0].Name)
|
||||
change.shaAfter = ParseRevision(payload.Resource.RefUpdates[0].NewObjectID)
|
||||
change.shaBefore = ParseRevision(payload.Resource.RefUpdates[0].OldObjectID)
|
||||
touchedHead = payload.Resource.RefUpdates[0].Name == payload.Resource.Repository.DefaultBranch
|
||||
if len(payload.Resource.RefUpdates) > 0 {
|
||||
revision = ParseRevision(payload.Resource.RefUpdates[0].Name)
|
||||
change.shaAfter = ParseRevision(payload.Resource.RefUpdates[0].NewObjectID)
|
||||
change.shaBefore = ParseRevision(payload.Resource.RefUpdates[0].OldObjectID)
|
||||
touchedHead = payload.Resource.RefUpdates[0].Name == payload.Resource.Repository.DefaultBranch
|
||||
}
|
||||
// unfortunately, Azure DevOps doesn't provide a list of changed files
|
||||
case github.PushPayload:
|
||||
// See: https://developer.github.com/v3/activity/events/types/#pushevent
|
||||
@@ -255,13 +261,15 @@ func (a *ArgoCDWebhookHandler) affectedRevisionInfo(payloadIf any) (webURLs []st
|
||||
|
||||
// Webhook module does not parse the inner links
|
||||
if payload.Repository.Links != nil {
|
||||
for _, l := range payload.Repository.Links["clone"].([]any) {
|
||||
link := l.(map[string]any)
|
||||
if link["name"] == "http" {
|
||||
webURLs = append(webURLs, link["href"].(string))
|
||||
}
|
||||
if link["name"] == "ssh" {
|
||||
webURLs = append(webURLs, link["href"].(string))
|
||||
clone, ok := payload.Repository.Links["clone"].([]any)
|
||||
if ok {
|
||||
for _, l := range clone {
|
||||
link := l.(map[string]any)
|
||||
if link["name"] == "http" || link["name"] == "ssh" {
|
||||
if href, ok := link["href"].(string); ok {
|
||||
webURLs = append(webURLs, href)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,11 +288,13 @@ func (a *ArgoCDWebhookHandler) affectedRevisionInfo(payloadIf any) (webURLs []st
|
||||
// so we cannot update changedFiles for this type of payload
|
||||
|
||||
case gogsclient.PushPayload:
|
||||
webURLs = append(webURLs, payload.Repo.HTMLURL)
|
||||
revision = ParseRevision(payload.Ref)
|
||||
change.shaAfter = ParseRevision(payload.After)
|
||||
change.shaBefore = ParseRevision(payload.Before)
|
||||
touchedHead = bool(payload.Repo.DefaultBranch == revision)
|
||||
if payload.Repo != nil {
|
||||
webURLs = append(webURLs, payload.Repo.HTMLURL)
|
||||
touchedHead = payload.Repo.DefaultBranch == revision
|
||||
}
|
||||
for _, commit := range payload.Commits {
|
||||
changedFiles = append(changedFiles, commit.Added...)
|
||||
changedFiles = append(changedFiles, commit.Modified...)
|
||||
@@ -320,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
|
||||
}
|
||||
|
||||
@@ -352,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -439,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)
|
||||
@@ -462,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 {
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/webhooks/v6/azuredevops"
|
||||
|
||||
bb "github.com/ktrysmt/go-bitbucket"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
@@ -42,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"
|
||||
@@ -180,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) {
|
||||
@@ -336,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{})
|
||||
@@ -644,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
|
||||
}
|
||||
|
||||
@@ -728,6 +608,26 @@ func Test_affectedRevisionInfo_appRevisionHasChanged(t *testing.T) {
|
||||
{true, "refs/tags/no-slashes", bitbucketPushPayload("no-slashes"), "bitbucket push branch or tag name without slashes, targetRevision tag prefixed"},
|
||||
{true, "refs/tags/no-slashes", bitbucketRefChangedPayload("no-slashes"), "bitbucket ref changed branch or tag name without slashes, targetRevision tag prefixed"},
|
||||
{true, "refs/tags/no-slashes", gogsPushPayload("no-slashes"), "gogs push branch or tag name without slashes, targetRevision tag prefixed"},
|
||||
|
||||
// Tests fix for https://github.com/argoproj/argo-cd/security/advisories/GHSA-wp4p-9pxh-cgx2
|
||||
{true, "test", gogsclient.PushPayload{Ref: "test", Repo: nil}, "gogs push branch with nil repo in payload"},
|
||||
|
||||
// Testing fix for https://github.com/argoproj/argo-cd/security/advisories/GHSA-gpx4-37g2-c8pv
|
||||
{false, "test", azuredevops.GitPushEvent{Resource: azuredevops.Resource{RefUpdates: []azuredevops.RefUpdate{}}}, "Azure DevOps malformed push event with no ref updates"},
|
||||
|
||||
{true, "some-ref", bitbucketserver.RepositoryReferenceChangedPayload{
|
||||
Changes: []bitbucketserver.RepositoryChange{
|
||||
{Reference: bitbucketserver.RepositoryReference{ID: "refs/heads/some-ref"}},
|
||||
},
|
||||
Repository: bitbucketserver.Repository{Links: map[string]any{"clone": "boom"}}, // The string "boom" here is what previously caused a panic.
|
||||
}, "bitbucket push branch or tag name, malformed link"}, // https://github.com/argoproj/argo-cd/security/advisories/GHSA-f9gq-prrc-hrhc
|
||||
|
||||
{true, "some-ref", bitbucketserver.RepositoryReferenceChangedPayload{
|
||||
Changes: []bitbucketserver.RepositoryChange{
|
||||
{Reference: bitbucketserver.RepositoryReference{ID: "refs/heads/some-ref"}},
|
||||
},
|
||||
Repository: bitbucketserver.Repository{Links: map[string]any{"clone": []any{map[string]any{"name": "http", "href": []string{}}}}}, // The href as an empty array is what previously caused a panic.
|
||||
}, "bitbucket push branch or tag name, malformed href"},
|
||||
}
|
||||
for _, testCase := range tests {
|
||||
testCopy := testCase
|
||||
@@ -856,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()
|
||||
@@ -900,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
|
||||
}
|
||||
|
||||
@@ -1216,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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user